树莓派简单操作系统制作之二:树莓派4B裸机程序的编译

在上一篇教程中,我们已经可以实现通过串口进入我们自己编译的U-boot的终端命令界面。本文将介绍后续代码结构以及编译的规则。

介绍

首先,因为处理器仅能处理数据,因此我们直接将我们想要做的事情描述成文字,放到板子里是不能正常被运行的。我们通过写代码让树莓派执行我们希望它做的事情,并且代码是不能直接被执行的,需要使用交叉编译工具按照一定的格式将我们写的程序编译成处理器认识的形式来执行。具体步骤如下:

  1. 首先编译程序,编写交叉编译器的编译及链接规则,并最终编译成处理器可以执行的二进制文件
  2. 将可执行的二进制文件加载到系统内存(RAM)中的某一块地址上。
  3. 处理器跳转到指定的内存地址上(即可执行的二进制文件所在的地址)执行。
    本篇文章主要讲的是第一步,如何按照一定的规则将程序编译成处理器可以执行的二进制文件。

项目结构

后续所有章节的源码码都有相似的结构,在这进行简单介绍:
cfg目录,包含一些配置文件,在这主要为树莓派需要的config.txt 文件。
include目录:包含源码编译所需要的头文件
src目录:包含所有源码
Makefile:因为使用make工具编译我们的系统,所以需要编写make工具的配置文件Makefile文件来编译并链接我们的源码。

程序文件介绍

Makefile

首先简单介绍一下Makefile的内容,因为make工具通过Makefile配置文件编译整个项目,因此通过这个可以了解整个项目结构。关于make工具可以通过这个网页了解:make
本项目中Makefile的内容如下:

CROSS_TOOL ?= aarch64-rpi4-linux-gnu-
CFLAGS = -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMFLAGS = -Iinclude
BUILD_DIR = build
SRC_DIR = src

all : kernel8.img

clean :
        rm -rf $(BUILD_DIR) *.img

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
        mkdir -p $(@D)
        $(CROSS_TOOL)gcc $(CFLAGS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
        $(CROSS_TOOL)gcc $(ASMFLAGS) -MMD -c $< -o $@

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

kernel8.img: $(SRC_DIR)/link.ld $(OBJ_FILES)
        $(CROSS_TOOL)ld -T $(SRC_DIR)/link.ld -o $(BUILD_DIR)/kernel8.elf  $(OBJ_FILES)
        $(CROSS_TOOL)objcopy $(BUILD_DIR)/kernel8.elf -O binary kernel8.img

简单介绍一下内容:

CROSS_TOOL ?= aarch64-rpi4-linux-gnu-

CROSS_TOOL 代表编译工具的前缀,因为我们是在x86主机上编译arm64的应用,因此我们这里指向的是交叉编译器。

CFLAGS = -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMFLAGS = -Iinclude

CFLAGS 和ASMFLAGS为编译C程序和汇编ASM程序时需要传入的参数。参数的含义如下

  • -Wall 代表着编译的时候显示所有的warning
  • -nostdlib 表示不使用标准的C语言函数库(stdio.h等等)。因为绝大多数的函数库依赖于操作系统,我们这里编译的是一个裸机程序,不包含操作系统。
  • -nostartifiles 表示不适用标准的启动文件,启动文件包括设置初始的堆栈指针,初始化静态数据,并且最终跳到程序的主入口点。因为我们是裸机程序,这些需要我们自己完成。
  • -ffreestanding 表示独立的环境,独立环境表示标准库不存在的环境,编译器不假定函数名具有其通常的含义,比如main函数不一定是主函数的入口。
  • -Iinclude 表示程序编译所需要的头文件路径。
  • -mgeneral-regs-only 表示仅使用通用寄存器,ARM处理器通常也包含NEON 寄存器。作为一个裸机程序在这里,我们不希望也没必要编译器用到他们。
BUILD_DIR = build
SRC_DIR = src

BUILD_DIR 和 SRC_DIR 表示编译的中间文件和源码的目录名字。

all : kernel8.img

clean :
        rm -rf $(BUILD_DIR) *.img

这两个表示make的目标,其中all作为make的默认目标,当执行“make”不加参数的时候,默认会执行all 指定的目标(make一般默认执行第一个目标)。这里,我们的all的作用是将其指向另一个编译目标kernel8.img,clean则是用来清理编译的环境。

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
        mkdir -p $(@D)
        $(CROSS_TOOL)gcc $(CFLAGS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
        $(CROSS_TOOL)gcc $(ASMFLAGS) -MMD -c $< -o $@

以上两个目标,则是用来编译C程序和汇编程序的。比如在我们的源码目录“src”下有main.c 文件,则通过此编译命令生成main_c.o文件并存放在build目录中,同理在src中的汇编程序,比如boot.S也会编译成boot_s.o放在“build”目录中。$<和$@分别为在编译的时候输入和输出的文件名字,mkdir -p $(@D)作用是在编译之前创建编译完成后目标存放的目录,防止其不存在。
-MMD 参数。 该参数指示 gcc 编译器为每个生成的目标文件创建一个依赖文件。 依赖文件定义特定源文件的所有依赖项。 这些依赖项通常包含所有包含的标头的列表。 我们需要包含所有生成的依赖文件,以便 make 知道在标头更改的情况下究竟要重新编译什么。

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

C_FILES 表示源码中所有的C程序文件,ASM_FILES 表示源码中所有的汇编程序文件。ASM_FILES 表示每一个源文件与对应的编译“.o”文件的连接关系的集合。(关于替代关系,可参考:Substitution References

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

上面我们讲到-MMD参数在编译过程中会生成每个目标的依赖文件,即*.d文件。DEP_FILES即是这些依赖文件的集合。

kernel8.img: $(SRC_DIR)/link.ld $(OBJ_FILES)
        $(CROSS_TOOL)ld -T $(SRC_DIR)/link.ld -o $(BUILD_DIR)/kernel8.elf  $(OBJ_FILES)
        $(CROSS_TOOL)objcopy $(BUILD_DIR)/kernel8.elf -O binary kernel8.img

上述命令是将编译的*.o文件根据链接脚本link.ld进行链接。link.ld定义了最后编译生成的镜像的基本布局(后面会讲解)。
链接完成后会生成kernel8.elf的文件,但是ELF文件时被设计为在操作系统内执行的可执行程序,我们要编译成裸机程序,因此需要将程序中所有的data数据和可执行的程序部分提取到kernel8.img文件中,至于为什么kernel8.img以8结尾,大概是树莓派定义的规则,8代表ARMv8,使树莓派默认以64位模式运行。同样的也可以修改配置文件config.txt中对应的arm_control参数来指定树莓派ARM的工作模式。但位了避免不必要的麻烦,这里直接将镜像名字定义成kernel8.img。

链接脚本link.ld

链接脚本的目的是将源程序编译生成的.o 文件,通过脚本中定义的规则映射到输出文件.elf文件中。内容如下:

SECTIONS
{
    . = 0x80000;
    .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;

系统上电后,首先进入uboot的命令行界面(上一章节已经重点介绍),在uboot中我们会加载我们的kernel8.img镜像文件,并且从镜像文件的开始处执行我们的程序,所以我们必须要保证我们的第一部分要执行的代码(.text.boot部分)放在开头的位置。.text、.rodata 和 .data 部分包含内核编译的指令、只读数据和普通数据。 .bss 部分包含应初始化为 0 的数据。

启动内核

我们将内核启动汇编程序放在start.S文件中,其内容如下:

.section ".text.boot" // make sure this put first section in kernel img

.global _start

_start: //story begin
// first there is four cpus in the raspberry, but we only use the first one (wich processor id is 0x0) at the start time
    mrs     x1, mpidr_el1 // get current cpu processor id and save to x1
    and     x1, x1, #3 // check if the processor id is 0x0
    cbz     x1, primary_processor // if primary_processor, jump primary_processor
    b       processor_hang

processor_hang: //if not primary_processor hang it by looping here
    wfe     // assure processor not used to low power mode
    b       processor_hang
primary_processor:
    // to Set sp to our _start
    ldr     x1, =_start
    mov     sp, x1

    // init bss section by set the bss section memary to zero
    ldr     x1, = __bss_start
    ldr     w2, = __bss_size
clean_bss:
    cbz     w2, end_clean_bss
    str     xzr, [x1], #8
    sub     w2, w2, #1
    cbnz    w2, clean_bss

end_clean_bss:
    bl      main   //jump to C main function
    //after main() excution, cpu would hang
    b       processor_hang

下面将对程序进行具体介绍:

.section ".text.boot" // make sure this put first section in kernel img

.global _start

_start: //story begin

我们把在此文件的所有内容都放在.text.boot段中,结合上一节讲到的链接文件。确保u-boot加载此程序后首先执行此文件中定义的_start函数。

    mrs     x1, mpidr_el1 // get current cpu processor id and save to x1
    and     x1, x1, #3 // check if the processor id is 0x0
    cbz     x1, primary_processor // if primary_processor, jump primary_processor
    b       processor_hang

树莓派4B用到的处理器BM2711有4个处理器核心,程序开始执行后,在4个处理器核心上同时开始执行,boot阶段只用到第一个处理器,因此第一部分代码就是挂起掉其它非0处理器。mpidr_el1 寄存器保存了执行程序的处理器id号。获取id后,如果是非0号的处理器核心则执行以下代码挂起。

processor_hang: //if not primary_processor hang it by looping here
    wfe     // assure processor not used to low power mode
    b       processor_hang

wfe 指令可以设置处理器核心进入低功耗模式。如果在第一步中检查处理器id的时候判断为第一个处理器核心,则执行以下代码:

primary_processor:
    // to Set sp to our _start
    ldr     x1, =_start
    mov     sp, x1

    // init bss section by set the bss section memary to zero
    ldr     x1, = __bss_start
    ldr     w2, = __bss_size
clean_bss:
    cbz     w2, end_clean_bss
    str     xzr, [x1], #8
    sub     w2, w2, #1
    cbnz    w2, clean_bss

end_clean_bss:
    bl      main   //jump to C main function
    //after main() excution, cpu would hang
    b       processor_hang

此段代码,首先先将_start函数所在的地址赋给堆栈指针sp。然后处理.bss段, 将全部内容重置为0。最后执行bl system_main指令跳转到C程序里面去执行。

main.c 程序

main.c 为程序初始化完成后跳转的第一个C语言程序,上一小节汇编程序初始化完成后会通过bl指令跳转到system_main函数执行,此函数定义在main.c程序文件中,内容如下:

void system_main(void)
{
    
    
        while (1){
    
    
        }
}

因为写的是裸机程序,裸机系统中,通常会写一个死循环使程序留在系统中,如上所示。

编译

最后,所有程序编写完成后,回到代码根目录。此时代码结构如下:

$ tree
.
├── include
├── Makefile
└── src
    ├── link.ld
    ├── main.c
    └── start.S

2 directories, 4 files

在代码根目录中,执行make命令便会在目录中生成kernel8.img文件。过程中需要安装对应的编译工具及相关依赖,make等。

猜你喜欢

转载自blog.csdn.net/weixin_43328157/article/details/130421718