• 编译器是如何将芯片执行的第一个指令放到芯片起始地址的?


    编译器是如何将芯片执行的第一个指令放到芯片起始地址的?

    芯片上电后,会自动跳到第一需要执行的指令,那么编译器和链接器是做了哪些工作才让第一条指令被放在了正确的地方,以arm为例,uboot编译后为何能确保reset被放在了起始地址呢?

    本人8年嵌入式Linux BSP开发经验,关于U-Boot的开发经验如下:

    ·平台开发:涉及ARM32/64、MIPS架构,MTK、海思等不下5个厂商U-Boot的BSP开发

    ·版本升级:将某平台的低版本U-boot升级至最新u-boot-2022.01版本

    ·架构设计:将业务代码从U-Boot中剥离,灵活适配不同U-boot版本、所有产品不同平台不同架构的不同单板

    所以,相信有资格回答这个问题。

    本文8000多字,掏心带你深入理解背后的原理,需要你耐心往下看。除了问题本身,你还将收获:

    ·什么是链接脚本,U-Boot下的链接脚本长什么样?

    ·U-Boot编译时是怎么链接的?

    ·U-Boot下这么多start.s,当前设备跑的到底是哪一个?

    正文

    这个问题往深了说就涉及到了编译原理。

    在计算机世界中,其实每一次链接过程都是由链接脚本控制的。那么什么是链接脚本呢?简单说就是由链接器命令语言书写的,给链接器看的,主要的目的是描述输入文件中的段(如text段、data段、bss段等)如何在输出文件中组装,并控制输出文件的存储布局。

    那么上面提到的链接器、链接脚本、输入文件、输出文件分别是指什么呢?

    读万卷书,不如走一步路。动动手指,编译一下U-Boot就知道了,如果你还不清楚怎么编译或者没有环境的话,建议你参考我的文章搭建一个:

    闪光吧Linux:走进嵌入式Linux大门的第二步——构建最新u-boot学习环境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.bfd。链接脚本就是u-boot.lds,用“-T”命令行选项来指定。输入文件就是各个目录下编译好的build-in.o以及lib库。输出文件就是u-boot,通过“-o”命令行选项来指定。

    千呼万唤始出来,我们来看看u-boot.lds这个链接脚本长的帅不帅:

    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*) }

    }

    咋一看是不是挺帅的,好,那我们来认识一下这位大帅哥。

    他首先是一个文本文件,里面有一系列命令。其中SECTIONS该命令用于描述输出文件的内存布局,它后面跟着花括号中的一些列符号分配和输出段的描述。该命令的第一行设定了特殊符号“.”的值,“.”值是位置计数器。如果不用其他方式指定输出段的地址,地址从位置计数器的当前值开始计算。在SECTIONS命令的最开始,位置计数器的值是0。

    接下来定义输出段“.text”。冒号是必需的格式。在输出段的名称后有一个花括号,里面可以列举放入该输出段的输入段。“*”是匹配任何文件名的通配符。表达式“*(.__image_copy_start)”表示所有输入文件中的所有“.__image_copy_start”输入段。接下来是表达式“arch/arm/cpu/armv8/start.o(.text*)”表示所有输入文件中的“.text”段的arch/arm/cpu/armv8/start.o。

    题主的关切点来了:

    U-Boot执行的第一条指令用链接脚本中的术语叫做“入口点”。链接脚本中使用ENTRY命令来设置。参数是一个符号名称:ENTRY(symol),这里的符号就是"_start"。这里的“_start”是什么呢?就是0x0,查看方式如下:

    80bf70804658505322cde52c838b456e.png

    那么0x0后面放什么呢?摘取关键部分如下:

     . = 0x00000000;

     . = ALIGN(8);

     .text :

     {

      *(.__image_copy_start)

      arch/arm/cpu/armv8/start.o (.text*)

     }

    0x0后面放text段,text段里面先放什么呢?就是“*(.__image_copy_start)”,是什么呢,还是0x0:

    35435d5313ca6bc0ad1509ee8e785236.png

    下面就是主角的真面目了,就是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 reset”就会放在存储介质的0地址了吧,圆满解答题主的问题。

    2022/04/03更新(没想到阅读量已快1千,感谢知友的赞同和追更):

    关于链接脚本的入口点的补充:

    前面提到,可以使用ENTRY链接脚本命令设置入口点。其实链接器支持多种方式设置入口点,会按照如下优先顺序尝试设定入口点:

    .“-e”入口命令行选项。

    .链接脚本中的ENTRY(symbol)命令。

    .已经定义的目标特定符号的值。通常是start。

    .链接脚本段中第一个字节的地址。

    .地址0。

    光说不练假把式,实验来检验。

    实验一:屏蔽链接脚本中的“ENTRY(_start)"和"arch/arm/cpu/armv8/start.o"

    9213736bf1b4476eede333ef8aa5f5dd.png

    重新编译后,查看映射表不再是从“b reset”开始了,直接是“__arm_smccc_smc”函数:

    $ 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,而.text段描述并没有指定具体内容,顺着肯定就是.efi_runtime段的第一个字节了。

    实验二:保留ENTRY(_start),并将“arch/arm/cpu/armv8/start.o”替换为“*(.text)*”:

    f1c4d59de8d0d95300e875207ebe2309.png

    这种情况扔能通过"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)*”:

    07bdb7bc025477bcead66f6a676474a5.png

    扔能找到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。

    剩下的实验就交给你啦。

  • 相关阅读:
    CNM:MERGE INTO
    【Java----String类详解】
    【科学计算与可视化】3. Matplotlib 绘图基础
    数据库-索引
    【Python零基础入门篇 · 24】:面向对象的多态、静态方法和类方法
    LeetCode70. 爬楼梯(C++动态规划简单题)
    汽车企业如何高效运营私域流量?
    【MQTT从入门到提高系列 | 09】WireShark抓包分析MQTT报文
    Gitee整改之思考
    jdk 管理工具比对 jEnv jabba SDKMAN
  • 原文地址:https://blog.csdn.net/danpianji777/article/details/125435582