赞
踩
在上一篇教程中,我们已经可以实现通过串口进入我们自己编译的U-boot的终端命令界面。本文将介绍后续代码结构以及编译的规则。
首先,因为处理器仅能处理数据,因此我们直接将我们想要做的事情描述成文字,放到板子里是不能正常被运行的。我们通过写代码让树莓派执行我们希望它做的事情,并且代码是不能直接被执行的,需要使用交叉编译工具按照一定的格式将我们写的程序编译成处理器认识的形式来执行。具体步骤如下:
后续所有章节的源码码都有相似的结构,在这进行简单介绍:
cfg目录,包含一些配置文件,在这主要为树莓派需要的config.txt 文件。
include目录:包含源码编译所需要的头文件
src目录:包含所有源码
Makefile:因为使用make工具编译我们的系统,所以需要编写make工具的配置文件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程序时需要传入的参数。参数的含义如下
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。
链接脚本的目的是将源程序编译生成的.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 为程序初始化完成后跳转的第一个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等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。