rpi4-osdev-学习开发日志-1

前言

已经完成了复试,也不知道南大什么时候才能下发拟录取通知。虽然依旧无法认为自己可以百分百进入南大,但至少目前来看希望还是蛮大的。总之先找点事情干,等到成绩出来之后再开始做毕设也不算迟。(ps:把文章搬过来的时候成绩已经公布了,已经进南大了,还是比较开心的)

今天要尝试的项目是在裸树莓派4B上开发一个OS。原项目地址是https://github.com/isometimes/rpi4-osdev ,正好手头也有一个早就吃灰的树莓派4B以及一块很小的hdmi屏幕,那自然恰好满足了开发的所有需求。

那么事不迟疑,开始我学习该项目的第一部分。

温馨提示:这里边肯定有很多很多错误或者莫名其妙的理解思路,所以仅作展示用,估计拿这玩意学习会把大伙带坑里。有什么问题直接在评论区指出即可,我会修改有问题的部分的。

温馨提醒

请注意,我现在写的基本思路就是:大概的读一读,并摘抄部分片段予以翻译,并非完全不翻译或完全机翻并搬过来。所以说主观色彩特别严重,若介意的话也请前往项目页面自行阅读。

阅读这系列博客需要一定量的计算机基础知识,我列举一些仅供参考:基本的计算机组成原理知识,即你需要大概的知道你手头的计算机到底是怎么工作的;基本的计算机操作系统的知识;一定的C语言基础与汇编语言基础;懂得如何给你手头的树莓派安装系统。

那么,开始吧!!

Introduction

These are all operating systems - software designed to make computer chips work out of the box for mere mortals like you and me.

这些就是计算机系统——一种可以让计算机芯片以及其附属硬件对于我们这群人起到开箱即用的软件。

这里其实就是计算机系统的一个初衷。摘抄下来是因为这里的英文表述非常的好玩。

Hardware Prerequisites

  1. 一块树莓派4B(这里可能需要一根专门的树莓派HDMI线,请购买前仔细观察清楚);
  2. 一个屏幕(HDMI接口);
  3. 一块microSD卡(当然,读卡器是必须的,要不然写个锤子系统);
  4. 写代码的机器。
  5. 很大概率你需要的一个东西:USB to Serial TTL线。(具体原因是因为,如果你想调试这玩意,后期可以通过hdmi线输出到屏幕上,但前期估计很困难,故需要这个来通过树莓派的输出信号来debug。)

Part 1 Bootstrapping

在运行我们的kernel程序之前,我们自然需要考虑“载入”我们的kernel程序。那么Bootstrap就可以做到这件事情。所以这一部分可以说我们“不可避免”的需要写一些Assembly code(事实上,确实有些观点是我们实际上不需要编写任何Assembly code就可以,但这里我们顺着原文意思来)。

先进行一些科普:

  • bss段(bss segment):bss是Block Started by Symbol的简称,用来存放程序中未初始化的全局变量的内存区域,属于静态内存分配。
  • data段(data segment):用来存放程序中已初始化的全局变量的内存区域,属于静态内存分配。
  • text段(text segment):用来存放程序执行代码的内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。也有可能包含一些只读的常数变量,例如字符串常量等。
  • 堆(heap):用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
  • 栈(stack):用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在data段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。

bss段与data段的区别

在初始化时 bss 段部分将会清零。bss 段属于静态内存分配,即程序一开始就将其清零了。
比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

  • text 和 data 段都在可执行文件中,由系统从可执行文件中加载;
  • 而 bss 段不在可执行文件中,由系统初始化。

作者:JamFF
链接:https://www.jianshu.com/p/ddfb284c1f7a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

由上述内容可知,bss段的内容由os负责初始化,其目的是在于节省一定的空间。(此观点来源于网络,并未深究,若有问题后续会进行修改。)

接下来我们就得知道Bootstrap需要考虑什么东西。

  • 树莓派所使用的CPU是Arm架构的Cortex-A72芯片,这其中有四核。我们自然希望kernel要运行在主核心上(来自后来的补充:貌似这里也可以理解成主进程上)。所以我们需要让代码检查一下处理器id,若运行在主核心上便继续运行,若运行在其他核心上边那就死循环。
  • OS并不太清楚栈区在哪。我们得设法告诉OS这一点。
  • OS需要想方设法初始化BSS section。一种行之有效的方式就是直接搞成全0,而不是想方设法去写什么结构体啥的来进行空间划分。
  • 接下来自然是运行main函数。

我们在之前提到这一点过了,就是说一开始我们迫不得已必须写一点汇编。ARM的ISA自然是不可避免的要学习的,这里给出一个原文作者使用的Guide:https://developer.arm.com/documentation/den0024/a/

接下来就是我们的第一个文件了:

// boot.S

.section ".text.boot"  // Make sure the linker puts this at the start of the kernel image

.global _start  // Execution starts here

_start:
    // Check processor ID is zero (executing on main core), else hang
    mrs     x1, mpidr_el1
    and     x1, x1, #3
    cbz     x1, 2f
    // We're not on the main core, so hang in an infinite wait loop
1:  wfe
    b       1b
2:  // We're on the main core!

    // Set stack to start below our code
    ldr     x1, =_start
    mov     sp, x1

    // Clean the BSS section
    ldr     x1, =__bss_start     // Start address
    ldr     w2, =__bss_size      // Size of the section
3:  cbz     w2, 4f               // Quit loop if zero
    str     xzr, [x1], #8
    sub     w2, w2, #1
    cbnz    w2, 3b               // Loop if non-zero

    // Jump to our main() routine in C (make sure it doesn't return)
4:  bl      main
    // In case it does return, halt the master core too
    b       1b

这一段是直接抄下来的。我当然看不懂:这玩意是arm架构的,写出这些代码虽然不需要完全通晓arm的所有指令,但依旧需要知道大概的编写思路。

当然,如果只是单纯的为了娱乐,那么自然没必要像我这样把这里的每一个字都读懂。但是,为了未来有可能的进一步开发(娱乐),那自然是提前学学为好。

给出的那个链接自然是非常晦涩难懂的。所以这里会综合一下网络上我能找到的资源来进行相关内容的编写(甚至最多甩个链接),也供我自己未来的复习。

那么树莓派4B到底用的什么芯片?

事实上,树莓派使用的是BCM2711芯片。官网给出的解释如下。
This is the Broadcom chip used in the Raspberry Pi 4 Model B, the Raspberry Pi 400, and the Raspberry Pi Compute Module 4. The architecture of the BCM2711 is a considerable upgrade on that used by the SoCs in earlier Raspberry Pi models. It continues the quad-core CPU design of the BCM2837, but uses the more powerful ARM A72 core. It has a greatly improved GPU feature set with much faster input/output, due to the incorporation of a PCIe link that connects the USB 2 and USB 3 ports, and a natively attached Ethernet controller. It is also capable of addressing more memory than the SoCs used before.

The ARM cores are capable of running at up to 1.5 GHz, making the Raspberry Pi 4 about 50% faster than the Raspberry Pi 3B+. The new VideoCore VI 3D unit now runs at up to 500 MHz. The ARM cores are 64-bit, and while the VideoCore is 32-bit, there is a new Memory Management Unit, which means it can access more memory than previous versions.

The BCM2711 chip continues to use the heat spreading technology started with the BCM2837B0, which provides better thermal management.

Processor: Quad-core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5 GHz.
Memory: Accesses up to 8GB LPDDR4-2400 SDRAM (depending on model)
Caches: 32kB data + 48kB instruction L1 cache per core. 1MB L2 cache.
Multimedia: H.265 (4Kp60 decode); H.264 (1080p60 decode, 1080p30 encode); OpenGL ES, 3.0 graphics
I/O: PCIe bus, onboard Ethernet port, 2 × DSI ports (only one exposed on Raspberry Pi 4B), 2 × CSI ports (only one exposed on Raspberry Pi 4B), up to 6 × I2C, up to 6 × UART (muxed with I2C), up to 6 × SPI (only five exposed on Raspberry Pi 4B), dual HDMI video output, composite video output.

那么这里其实有一个蛮有意思的小知识点。MCU其实就是我们熟知的单片机,而这个BCM2711则是一个SoC。

以下观点摘自https://zhuanlan.zhihu.com/p/516314955

低端的SOC就是内部集成了MCU+特定功能模块外设。
高端的SOC应该是内部集成MPU/CPU+特定功能模块外设,高端的我也没用过,我猜的,今天我们低端的SOC。

所以说,我们只需要关注ARM架构所需要学习的ISA即可。

ARM Cortex-A系列CPU指令集学习小集

上点链接,大概就是ARM指令的举例与介绍:
https://blog.csdn.net/weixin_41898804/article/details/105789011

关于ARM aarch64的寄存器的文章:
https://blog.csdn.net/weixin_42135087/article/details/111263720

看到上边提到了交叉编译,那就不得不去学习一下GNU风格的汇编语法,来熟悉相关内容。上链接:https://blog.csdn.net/daocaokafei/article/details/115439936

一些常见的ARM指令的合集,当然包括了其中的标志寄存器:https://blog.csdn.net/liyuewuwunaile/article/details/107307347

和着已经有前辈帮忙做了这一块内容了啊,那没事了:https://blog.csdn.net/qq_23320955/article/details/114228188

那么其实这里就可以给出一些目前我能给出的理解了。(毕竟是学习向,不能指望我一次就学对东西)

也就是说,由于我们需要交叉编译代码,再加之我们想要借助的交叉编译平台是gnu工具链,所以说需要按照GNU给出的格式编写对应的代码。其实大部分的汇编部分自然是按照对应平台的指令集进行编写,其余部分则通过GNU伪指令的方式来辅助汇编器生成对应的目标代码。

所以说伪指令是必须要学的,这样就可以通过现成的工具来实现我们的os内核的编写。
伪指令相关的链接:https://blog.csdn.net/Roland_Sun/article/details/107705952

这里再补充一个ARM指令小抄:https://zhuanlan.zhihu.com/p/164415889

代码逐行解读

tmd markdown不渲染asm的

.section ".text.boot"  // 这里相当于标识了当前段为何。.text是个固定表达,在编译器中是真的会有相应动作的,但.boot个人认为就是为了待会在编写ld时确保能挂载到起始位置,故意留下来的一个标志段。

.global _start  // 这个貌似就是固定表达

_start:
    // (原文翻译)该段代码检查当前所处位置是否为核心态。若是则接着执行,否则直接无限循环。
    mrs     x1, mpidr_el1 // 详看注1和注2。
    and     x1, x1, #3 // 把上述值与0b11对比一下。
    cbz     x1, 2f // 若为零则跳入2f。也就是说是主核心+主线程。至于为什么是2f见注3。
    // We're not on the main core, so hang in an infinite wait loop
1:  wfe // wait for event。具体见:https://blog.csdn.net/Roland_Sun/article/details/107456179
    b       1b
2:  // 主核心部分

    // Set stack to start below our code
    ldr     x1, =_start // 为啥写=,见注4
    mov     sp, x1 // 一定别忘了寄存器组长啥样。图片不容易放出,但这里可以简单讲一下:r13(或称为SP),r14(或称为LR,也就是连接寄存器),r15(或称为PC)

    // Clean the BSS section
    ldr     x1, =__bss_start     // Start address
    ldr     w2, =__bss_size      // Size of the section(w2寄存器即r2寄存器的前32位)
3:  cbz     w2, 4f               // Quit loop if zero
    str     xzr, [x1], #8        // store指令。该指令比较特别,大概长这样STR 源寄存器,<存储器地址>,并且会伴随一些额外操作,比如做完存储后会对x1中的数值自动加8。xzr即0寄存器,即里边只有零,对应的wzr也是半长寄存器,里边全是0。
    sub     w2, w2, #1
    cbnz    w2, 3b               // Loop if non-zero

    // Jump to our main() routine in C (make sure it doesn't return)
4:  bl      main
    // In case it does return, halt the master core too
    b       1b

注1:ARM 指令集提供了两条指令,可直接控制程序状态寄存器(Program State Register,PSR)。MRS 指令用于把 CPSR 或 SPSR 的值传送到一个寄存器;MSR 与之相反,把一个寄存器的内容传送到 CPSR 或 SPSR。这两条指令相结合,可用于对 CPSR 和 SPSR 进行读/写操作。程序状态寄存器指令如下表所示。

注2:mpidr_el1这个寄存器的注解也可以在https://developer.arm.com/documentation/ddi0595/2021-12/AArch64-Registers/MPIDR-EL1--Multiprocessor-Affinity-Register?lang=en
这个地方找到。当然,依旧有好事者把这玩意翻译成了中文:https://aijishu.com/a/1060000000380364
看不懂也没有关系,还有一篇文章帮助你来理解该部分内容:https://zhuanlan.zhihu.com/p/453687153

注3:label分为全局label和局部label。全局label自然是要以字母开头,其他任意的一种label。不能重复,也不能瞎写。但局部label就只能用0~99的数字作为标号。虽然确实允许重复,但也有诸多限制,在跳转到对应的局部标号的时候也要在后边加上操作类型以跳转到所需要的标签处。具体见:
https://juejin.cn/post/6907545027900080141
https://blog.51cto.com/u_7090376/1264642

注4:ldr指令的格式:
LDR R0, [R1]
LDR R0, =NAME
LDR R0, =0X123
对于第一种没有等号的情况,R1寄存器对应地址的数据被取出放入R0
对于第二种有等号的情况,R0寄存器的值将为NAME标号对应的地址。
对于第三种有等号的情况,R0寄存器的值将为立即数的值
————————————————
版权声明:本文为CSDN博主「Anciety」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_29343201/article/details/51604164

那么代码解读的部分也基本完成。这一段arm代码可以说也算得上是让我收获颇多,那么事不迟疑,接着下一段旅程。

main函数?

这里我们先不着急实现具体功能,先考虑写点简单东西。比如:

// kernel.c
void main(){
	while(1);
}

足够了!这就足够了,毕竟这一章写的是bootstrap,还没到写c的时候呢。

linker script

其实写到这里还是非常突兀的,因为完全不知道为啥要突然写出这个linker script。

对于链接器大伙可能有自己的一些想法。但若出现了诸如编写linux kernel的需求的时候,我们自然不能指望linker去想方设法自动地为我们选定地址。这就是为什么linker script诞生的原因。

事实上gcc官方自然是写了一个默认的linker scripts的。
这玩意自然是有官方文档的:因为是gnu开发的玩意:
http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html

放一个我认为非常好的博客:
https://www.cnblogs.com/idalink/articles/11800860.html

以及联系起你所认为的链接器与这个linker script的关系的文章:
https://cloud.tencent.com/developer/article/1690619

ld script简要学习

先读一遍官方文档。
原文我就不摘抄了,直接进行阅读与翻译。

Command Language & Linker Scripts

ld script可以对链接过程中的各个操作有一个非常精确的控制。它可以控制:

  • 输入文件
  • 文件格式
  • 输出文件格式
  • 各个section的地址
  • 普通块的放置

如果你想在链接过程中使用你写的script,那就使用-T指令后接你自己写的script吧。

ld命令语言实际上是一组状态,主要分为两种:利用关键字对特定选项进行设定,以及
对输入文件进行分组与选择或对输出文件命名。这两种指令就足以对链接过程搞出一片天了。
最常用的指令就是SECTIONS指令。这个指令大概以各种奇怪的角度描绘了输出文件布局是啥样的;MEMORY指令则是对SECTIONS指令的补充,主要就是描述了一下目标架构中可用空间是什么样的。当然啦,SECTIONS是必须添加的,MEMORY就无所谓了,若不加入MEMORY, ld就默认认为有足够的连续内存块留给输出。

补充的一句话:取名叫SECTIONS是有道理的,因为SECTIONS里定义的一堆SECTION。

表达式

正常的数字没啥可说的。
符号按道理来说只要符合通常认知的符号即可,但若出现了冲突或者非法字符,则需要用引号包起来即可。

地址计数器(Location Counter)

地址计数器是一个特殊的符号,这玩意是.。没错,就是一个点,这东西永远指向输出文件的某一点上。换句话说你可以认为这玩意就相当于输出头。当然,你可以对其进行算术操作,但请注意这些操作都会移动这个计数器。这东西只能增不能自减。

官方文档给了这个有意思的例子:

SECTIONS
{
  output :
  {
  file1(.text)
  . = . + 1000;
  file2(.text)
  . += 1000;
  file3(.text)
  } = 0x1234;
}

其中最后的 = 0x1234即会在那些空隙之地全部塞入0x1234。

请注意,ld script是惰性求值语言。

赋值,定义符号

这里有一个相当不错的解释:
https://www.cnblogs.com/god-of-death/p/14879078.html

我在这里摘抄一小段:

在链接文件中定义的变量(符号)可以在目标文件中使用

在链接文件中定义变量:

_init_start = .;
.application_init  : { *(.application_init) }
_init_end = .;

在源文件中使用变量_init_start:

#include <stdio.h>
#include <string.h>

struct _s_application_init {
    int(*function)(void);
};

extern struct _s_application_init _init_start;//段".application_init"的起始地址,在*.lds文件中定义
extern struct _s_application_init _init_end;//段".application_init"的末尾地址,在*.lds文件中定义

#define __app_init_section __attribute__((section(".application_init")))
#define __application_init(function) \
    struct _s_application_init _s_a_init_##function  __app_init_section = {function}

static int application_init_a(void)
{
    printf("execute funtion : %s\n", __FUNCTION__);
    return 0;
}
__application_init(application_init_a);int main(int argc, char **argv)
{
    /*
     * 从段的起始地址开始获取数据,直到末尾地址
     */

    struct _s_application_init *pf_init = &_init_start;
    do {
        printf("Load init function from address %p\n", pf_init);
        pf_init->function();
        ++pf_init;
    } while (pf_init < &_init_end);

    return 0;
}

(这上边这点是我后边补上的。)

所谓赋值语句其实就相当于给标号一个地址。原文如下:

You may create global symbols, and assign values (addresses) to global symbols, using any of the C assignment operators.

请注意,赋值只允许单符号在左边:a = b + 3;是合法表达,但a + b = 3;就不是了。而且每个赋值语句之后必须后接一个;

赋值语句要么单独成为一条指令,要么在SECTIONS指令中充当独立声明,要么作为SECTION定义内容的一部分。

赋值完事之后要么给一个绝对地址,要么给一个可重载地址。当然,赋值成哪种主要取决于赋值语句发生的位置在哪。其他位置嘛,就生成绝对地址了。若在SECTION里那就是相对地址(相对于段基址)。用ABSOLUTE()函数就可以将相对地址直接转成绝对地址。

先抄下来一段一个莫名其妙的话:

In some cases, it is desirable for a linker script to define a symbol only if it is referenced, and only if it is not defined by any object included in the link. For example, traditional linkers defined the symbol etext. However, ANSI C requires that the user be able to use etext as a function name without encountering an error. The PROVIDE keyword may be used to define a symbol, such as etext, only if it is referenced but not defined. The syntax is PROVIDE(symbol = expression).

一些奇怪的计算函数

有一些干脆自己去看手册得了。写点我觉得挺诡异的。

ALIGN(exp):返回一个当前位置计数器向着exp对其之后的地址。相当于,0x8001向着4对齐,那就是0x8004。官网写的那堆玩意根本看不懂。
有人对着ALIGN做了点实验:
https://blog.csdn.net/btoh_workstation/article/details/27510869

指定输出长啥样

实现这一点自然需要在SECTIONS中写好相应的段。那么如何写SECTIONS呢?
其实就是SECTIONS{...}即可。

如果不使用SECTIONS命令,链接器将每个输入节按照在输入文件中首次遇到的顺序放置到名称相同的输出节中。例如,如果所有输入节都出现在第一个文件中,那么输出文件中的节的顺序将与第一个输入文件中的顺序匹配。

在这大括号中,你可以干这三件事情:定义入口点,赋值给一个符号,描述指定输出部分的位置以及哪些输入部分进入其中。

在SECTIONS中最常用的声明就是SECTION,即段定义。段定义指定输出部分的属性:其位置、对齐、内容、填充模式和目标内存区域。格式为:secname : {contents},事实上secname周围必须要有空格,并且必须满足一定的约束。比如说,a.out只支持.text、.data以及.bss段,也就是说段名只能在这三个之中选。如果目标生成格式要求每个段名必须得是数字,那还得用双引号扣起来。

有一个特殊的保留段名叫/DISCARD/这玩意的作用就是排除那些不想放在生成目标文件的玩意。

请记住,linker是惰性的,故除非输入文件真的有符合content的段,否则根本不会生成相应段。

对于contents,相当于是指定文件、文件中对应的段,或者两者结合。当然,你可以在大括号中写好你需要写的东西,并用空格隔开。比如:.data : { afile.o bfile.o cfile.o }

具体里边有啥自个看文档去。稍微写几个特殊的。
filename(COMMON) *COMMON写出这俩相当于将特定文件(或所有文件中)未经初始化的数据放入该段之中。

当然,你不仅可以想方设法把那些文件中的段给放入content中,你也可以考虑直接写一些赋值语句,来将一些现成状态放入content中。

请注意。在content中进行赋值自然是偏向基址的。例子如下:

SECTIONS {
  abs = 14 ;
  ...
  .data : { ... rel = 14 ; ... }
  abs2 = 14 + ADDR(.data);
  ...
}

这里abs与rel表面值相同,但其实不然。abs2与rel才是相同的。

这里放入一个段声明的完整表达形式:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

BLOCK(align)就相当于该段首对其。
(NOLOAD)就是说不会载入内存。
AT那里没看懂,但是不着急,无所谓的。

关于ld script相当不错的文章:https://www.cnblogs.com/god-of-death/p/14879078.html

付诸实践

先来看一下源文档中所写的ld文件为何吧。

SECTIONS
{
    . = 0x80000;     /* Kernel load address for AArch64 */
    .text : { KEEP(*(.text.boot)) *(.text .text.* .gnu.linkonce.t*) }
    .rodata : { *(.rodata .rodata.* .gnu.linkonce.r*) }
    PROVIDE(_data = .);
    .data : { *(.data .data.* .gnu.linkonce.d*) }
    .bss (NOLOAD) : {
        . = ALIGN(16);
        __bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        __bss_end = .;
    }
    _end = .;

   /DISCARD/ : { *(.comment) *(.gnu*) *(.note*) *(.eh_frame*) }
}
__bss_size = (__bss_end - __bss_start)>>3;

首当其冲的自然是定义整个ld文件中最为重要的SECTIONS的声明语句。首先先定义载入点为0x80000,然后定义.text段。要知道我们需要针对目标机来选定我们到底要生成什么section,所以说这里是有讲究的。KEEP使得.text.boot段内的程序可以真的被排到0x80000上。接着是几个正常的段。
PROVIDE大概的意思就是,若程序中给出了_data的值,则使用其就好;但若程序未给出其定义但又希望引用这个的值,那么就采用其在ld script中定义的值就好。
.bss段稍微有点特殊,该段不需要加载入内存,并且先以16为标尺对齐好边界,定义好当前start,然后塞入一些乱七八糟的(包括未初始化的全局变量COMMON),然后定义一下end。最后定义一个__bss_size以供链接器使用。

先写到这里分一下,待会再把其余部分般一般。

posted @ 2023-04-03 17:25  Levia_than_www  阅读(137)  评论(0)    收藏  举报