编译器是如何将芯片执行的第一个指令放到芯片起始地址的?
编译器是如何将芯片执行的第一个指令放到芯片起始地址的?
芯片上电后,会自动跳到第一需要执行的指令,那么编译器和链接器是做了哪些工作才让第一条指令被放在了正确的地方, 以 ar m 为例, uboo t 编译后为何能确 保 rese t 被放在了起始地址呢?
本 人 8 年嵌入 式 Linux BS P 开发经验,关 于 U-Boo t 的开发经验如下:
· 平台开发:涉 及 ARM32/6 4 、 MIP S 架构 , MT K 、海思等不 下 5 个厂 商 U-Boo t 的 BS P 开发
· 版本升级:将某平台的低版 本 U-boo t 升级至最 新 u-boot-2022.0 1 版本
· 架构设计:将业务代码 从 U-Boo t 中剥离,灵活适配不 同 U-boo t 版本、所有产品不同平台不同架构的不同单板
所以,相信有资格回答这个问题。
本 文 800 0 多字,掏心带你深入理解背后的原理,需要你耐心往下看。除了问题本身,你还将收获:
· 什么是链接脚本 , U-Boo t 下的链接脚本长什么样?
· U-Boo t 编译时是怎么链接的?
· U-Boo t 下这么多 start.s ,当前设备跑的到底是哪一个?
正文
这个问题往深了说就涉及到了编译原理。
在计算机世界中,其实每一次链接过程都是由链接脚本控制的。那么什么是链接脚本呢?简单说就是由链接器命令语言书写的,给链接器看的,主要的目的是描述输入文件中的段( 如 tex t 段 、 dat a 段 、 bs s 段等)如何在输出文件中组装,并控制输出文件的存储布局。
那么上面提到的链接器、链接脚本、输入文件、输出文件分别是指什么呢?
读万卷书,不如走一步路。动动手指,编译一 下 U-Boo t 就知道了,如果你还不清楚怎么编译或者没有环境的话,建议你参考我的文章搭建一个:
闪光 吧 Linu x :走进嵌入 式 Linu x 大门的第二 步 — — 构建最 新 u-boo t 学习环境 9 赞 同 · 0 评论 文章
下面是编译结果,截取重点如下:
$ make CROSS_COMPILE=aarch64-linux-gnu- V=1
....
aarch64-linux-gnu-ld.bfd -pie --gc-sections -Bstatic --no-dynamic-linker -z notext --build-id=none -Ttext 0x00000000 -o u-boot -T u-boot.lds arch/arm/cpu/armv8/start.o --whole-archive arch/arm/cpu/built-in.o arch/arm/cpu/armv8/built-in.o arch/arm/lib/built-in.o board/emulation/common/built-in.o board/emulation/qemu-arm/built-in.o boot/built-in.o cmd/built-in.o common/built-in.o disk/built-in.o drivers/built-in.o drivers/usb/cdns3/built-in.o drivers/usb/common/built-in.o drivers/usb/dwc3/built-in.o drivers/usb/emul/built-in.o drivers/usb/eth/built-in.o drivers/usb/host/built-in.o drivers/usb/mtu3/built-in.o drivers/usb/musb-new/built-in.o drivers/usb/musb/built-in.o drivers/usb/phy/built-in.o drivers/usb/ulpi/built-in.o env/built-in.o fs/built-in.o lib/built-in.o net/built-in.o --no-whole-archive -L /usr/lib/gcc-cross/aarch64-linux-gnu/9 -lgcc -Map u-boot.map; true
aarch64-linux-gnu-objcopy --gap-fill=0xff -j .text -j .secure_text -j .secure_data -j .rodata -j .data -j .u_boot_list -j .rela.dyn -j .got -j .got.plt -j .binman_sym_table -j .text_rest -j .dtb.init.rodata -j .efi_runtime -j .efi_runtime_rel -O srec u-boot u-boot.srec
aarch64-linux-gnu-objcopy --gap-fill=0xff -j .text -j .secure_text -j .secure_data -j .rodata -j .data -j .u_boot_list -j .rela.dyn -j .got -j .got.plt -j .binman_sym_table -j .text_rest -j .dtb.init.rodata -j .efi_runtime -j .efi_runtime_rel -O binary u-boot u-boot-nodtb.bin && { echo " start=$(aarch64-linux-gnu-nm | grep __rel_dyn_start | cut -f 1 -d """ """); end=$(aarch64-linux-gnu-nm | grep __rel_dyn_end | cut -f 1 -d """ """); tools/relocate-rela $start $end"; start=$(aarch64-linux-gnu-nm u-boot | grep __rel_dyn_start | cut -f 1 -d " "); end=$(aarch64-linux-gnu-nm u-boot | grep __rel_dyn_end | cut -f 1 -d " "); tools/relocate-rela u-boot- nodtb.bin 0x00000000 $start $end; } || { rm -f u-boot-nodtb.bin; false; }
start=$(aarch64-linux-gnu-nm | grep __rel_dyn_start | cut -f 1 -d " "); end=$(aarch64-linux-gnu-nm | grep __rel_dyn_end | cut -f 1 -d " "); tools/relocate-rela $start $end
cp u-boot-nodtb.bin u-boot.bin
aarch64-linux-gnu-objdump -t u-boot > u-boot.sym
....
这里的链接器就 是 aarch64-linux-gnu-ld.bf d 。链接脚本就 是 u-boot.ld s , 用 " - T " 命令行选项来指定。输入文件就是各个目录下编译好 的 build-in. o 以 及 li b 库。输出文件就 是 u-boo t ,通 过 " - o " 命令行选项来指定。
千呼万唤始出来,我们来看 看 u-boot.ld s 这个链接脚本长的帅不帅:
linuxer@linuxer-virtual-machine:~/work/u-boot-2022.01$ vim u-boot.lds
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(8);
.text :
{
*(.__image_copy_start)
arch/arm/cpu/armv8/start.o (.text*)
}
.efi_runtime : {
__efi_runtime_start = .;
*(.text.efi_runtime*)
*(.rodata.efi_runtime*)
*(.data.efi_runtime*)
__efi_runtime_stop = .;
}
.text_rest :
{
*(.text*)
}
. = ALIGN(8);
. rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(8);
.data : {
*(.data*)
}
. = ALIGN(8);
. = .;
. = ALIGN(8);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(8);
.efi_runtime_rel : {
__efi_runtime_rel_start = .;
*(.rel*.efi_runtime)
*(.rel*.efi_runtime.*)
linuxer@linuxer-virtual-machine:~/work/u-boot-2022.01$ cat u-boot.lds
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(8);
.text :
{
*(.__image_copy_start)
arch/arm/cpu/armv8/start.o (.text*)
}
.efi_runtime : {
__efi_runtime_start = .;
*(.text.efi_runtime*)
*(.rodata.efi_runtime*)
*(.data.efi_runtime*)
__efi_runtime_stop = .;
}
.text_rest :
{
*(.text*)
}
. = ALIGN(8);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(8);
.data : {
*(.data*)
}
. = ALIGN(8);
. = .;
. = ALIGN(8);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(8);
.efi_runtime_rel : {
__efi_runtime_rel_start = .;
*(.rel*.efi_runtime)
*(.rel*.efi_runtime.*)
__efi_runtime_rel_stop = .;
}
. = ALIGN(8);
.image_copy_end :
{
*(.__image_copy_end)
}
. = ALIGN(8);
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rela. dyn : {
*(.rela*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
_end = .;
. = ALIGN(8);
.bss_start : {
KEEP(*(.__bss_start));
}
.bss : {
*(.bss*)
. = ALIGN(8);
}
.bss_end : {
KEEP(*(.__bss_end));
}
/DISCARD/ : { *(.dynsym) }
/DISCARD/ : { *(.dynstr*) }
/DISCARD/ : { *(.dynamic*) }
/DISCARD/ : { *(.plt*) }
/DISCARD/ : { *(.interp*) }
/DISCARD/ : { *(.gnu*) }
}
咋一看是不是挺帅的,好,那我们来认识一下这位大帅哥。
他首先是一个文本文件,里面有一系列命令。其 中 SECTION S 该命令用于描述输出文件的内存布局,它后面跟着花括号中的一些列符号分配和输出段的描述。该命令的第一行设定了特殊符 号 " . " 的值 , " . " 值是位置计数器。如果不用其他方式指定输出段的地址,地址从位置计数器的当前值开始计算。 在 SECTION S 命令的最开始,位置计数器的值 是 0 。
接下来定义输出 段 " .tex t " 。冒号是必需的格式。在输出段的名称后有一个花括号,里面可以列举放入该输出段的输入段 。 " * " 是匹配任何文件名的通配符。表达 式 " *(.__image_copy_start ) " 表示所有输入文件中的所 有 " .__image_copy_star t " 输入段。接下来是表达 式 " arch/arm/cpu/armv8/start.o ( .text * ) " 表示所有输入文件中 的 " .tex t " 段 的 arch/arm/cpu/armv8/start. o 。
题主的关切点来了:
U-Boo t 执行的第一条指令用链接脚本中的术语叫 做 " 入口点 " 。链接脚本中使 用 ENTR Y 命令来设置。参数是一个符号名称 : ENTRY(symol ) ,这里的符号就 是 "_start " 。这里 的 " _star t " 是什么呢?就 是 0x 0 ,查看方式如下:
那 么 0x 0 后面放什么呢?摘取关键部分如下:
. = 0x00000000 ;
. = ALIGN(8);
.text :
{
*(.__image_copy_start)
arch/arm/cpu/armv8/start.o (.text*)
}
0x 0 后面 放 tex t 段 , tex t 段里面先放什么呢?就 是 " *(.__image_copy_start ) " ,是什么呢,还 是 0x 0 :
下面就是主角的真面目了,就 是 arch/arm/cpu/armv8/start.o (.text* ) ,再揭开面纱一睹真容:
linuxer@linuxer-virtual-machine:~/work/u-boot-2022.01$ vim arch/arm/cpu/armv8/start.S
*************************************************************************/
.globl _start
_start:
b reset
.align 3
....
懂了吧,现在知道为 啥 " b rese t " 就会放在存储介质 的 0 地址了吧,圆满解答题主的问题。
2022/04/0 3 更新(没想到阅读量已 快 1 千,感谢知友的赞同和追更):
关于链接脚本的入口点的补充:
前面提到,可以使 用 ENTR Y 链接脚本命令设置入口点。其实链接器支持多种方式设置入口点,会按照如下优先顺序尝试设定入口点:
. " - e " 入口命令行选项。
. 链接脚本中 的 ENTR Y ( symbo l )命令。
. 已经定义的目标特定符号的值。通常 是 star t 。
. 链接脚本段中第一个字节的地址。
. 地 址 0 。
光说不练假把式,实验来检验。
实验一:屏蔽链接脚本中 的 " ENTRY(_start) " 和 "arch/arm/cpu/armv8/start.o"
重新编译后,查看映射表不再是 从 " b rese t " 开始了,直接 是 " __arm_smccc_sm c " 函数:
$ less System.map
0000000000000000 T __arm_smccc_smc
0000000000000000 T __efi_runtime_start
0000000000000000 T __image_copy_start
000000000000002c T __arm_smccc_hvc
0000000000000058 T invoke_psci_fn
00000000000000d4 T efi_reset_system
0000000000000130 W efi_get_time
0000000000000138 W efi_set_time
这种情况直接走顺 序 4 , 而 .tex t 段描述并没有指定具体内容,顺着肯定就 是 .efi_runtim e 段的第一个字节了。
实验二:保 留 ENTRY(_start ) ,并 将 " arch/arm/cpu/armv8/start. o " 替换 为 " *(.text) * " :
这种情况扔能通 过 "ENTRY(_start) " 找到对应入口:
$ less System.map
0000000000000000 T __image_copy_start
0000000000000000 T _start
0000000000000008 T _TEXT_BASE
0000000000000010 T _end_ofs
0000000000000018 T _bss_start_ofs
0000000000000020 T _bss_end_ofs
0000000000000028 t reset
000000000000002c T save_boot_params_ret
但无法直接看出是哪 个 . o ,实际确实 是 " arch/arm/cpu/armv8/start. o " ,因为只 有 "arch/arm/cpu/armv8/start.S " 定义 了 ".globl _start " 。
这种情况直接走顺 序 3 。
实验三:删 除 " ENTRY(_start) " ,并 将 " arch/arm/cpu/armv8/start. o " 替换 为 " *(.text) * " :
扔能找 到 arch/arm/cpu/armv8/start. S 作为入口:
$ less System.map
0000000000000000 T __image_copy_start
0000000000000000 T _start
0000000000000008 T _TEXT_BASE
0000000000000010 T _end_ofs
0000000000000018 T _bss_start_ofs
0000000000000020 T _bss_end_ofs
0000000000000028 t reset
000000000000002c T save_boot_params_ret
这种情况直接走顺 序 4 。
剩下的实验就交给你啦。