编译器是如何将芯片执行的第一个指令放到芯片起始地址的?
编译器是如何将芯片执行的第一个指令放到芯片起始地址的?
芯片上电后,会自动跳到第一需要执行的指令,那么编译器和链接器是做了哪些工作才让第一条指令被放在了正确的地方,以arm为例,uboot编译后为何能确保reset被放在了起始地址呢?
本人8年嵌入式LinuxBSP开发经验,关于UBoot的开发经验如下:
平台开发:涉及ARM3264、MIPS架构,MTK、海思等不下5个厂商UBoot的BSP开发
版本升级:将某平台的低版本Uboot升级至最新uboot2022。01版本
架构设计:将业务代码从UBoot中剥离,灵活适配不同Uboot版本、所有产品不同平台不同架构的不同单板
所以,相信有资格回答这个问题。
本文8000多字,掏心带你深入理解背后的原理,需要你耐心往下看。除了问题本身,你还将收获:
什么是链接脚本,UBoot下的链接脚本长什么样?
UBoot编译时是怎么链接的?
UBoot下这么多start。s,当前设备跑的到底是哪一个?
正文
这个问题往深了说就涉及到了编译原理。
在计算机世界中,其实每一次链接过程都是由链接脚本控制的。那么什么是链接脚本呢?简单说就是由链接器命令语言书写的,给链接器看的,主要的目的是描述输入文件中的段(如text段、data段、bss段等)如何在输出文件中组装,并控制输出文件的存储布局。
那么上面提到的链接器、链接脚本、输入文件、输出文件分别是指什么呢?
读万卷书,不如走一步路。动动手指,编译一下UBoot就知道了,如果你还不清楚怎么编译或者没有环境的话,建议你参考我的文章搭建一个:
闪光吧Linux:走进嵌入式Linux大门的第二步构建最新uboot学习环境9赞同0评论文章
下面是编译结果,截取重点如下:
makeCROSSCOMPILEaarch64linuxgnuV1
。。。。
aarch64linuxgnuld。bfdpiegcsectionsBstaticnodynamiclinkerznotextbuildidnoneTtext0x00000000oubootTuboot。ldsarcharmcpuarmv8start。owholearchivearcharmcpubuiltin。oarcharmcpuarmv8builtin。oarcharmlibbuiltin。oboardemulationcommonbuiltin。oboardemulationqemuarmbuiltin。obootbuiltin。ocmdbuiltin。ocommonbuiltin。odiskbuiltin。odriversbuiltin。odriversusbcdns3builtin。odriversusbcommonbuiltin。odriversusbdwc3builtin。odriversusbemulbuiltin。odriversusbethbuiltin。odriversusbhostbuiltin。odriversusbmtu3builtin。odriversusbmusbnewbuiltin。odriversusbmusbbuiltin。odriversusbphybuiltin。odriversusbulpibuiltin。oenvbuiltin。ofsbuiltin。olibbuiltin。onetbuiltin。onowholearchiveLusrlibgcccrossaarch64linuxgnu9lgccMapuboot。map;true
aarch64linuxgnuobjcopygapfill0xffj。textj。securetextj。securedataj。rodataj。dataj。ubootlistj。rela。dynj。gotj。got。pltj。binmansymtablej。textrestj。dtb。init。rodataj。efiruntimej。efiruntimerelOsrecubootuboot。srec
aarch64linuxgnuobjcopygapfill0xffj。textj。securetextj。securedataj。rodataj。dataj。ubootlistj。rela。dynj。gotj。got。pltj。binmansymtablej。textrestj。dtb。init。rodataj。efiruntimej。efiruntimerelObinaryubootubootnodtb。bin{echostart(aarch64linuxgnunmgrepreldynstartcutf1d);end(aarch64linuxgnunmgrepreldynendcutf1d);toolsrelocaterelastartend;start(aarch64linuxgnunmubootgrepreldynstartcutf1d);end(aarch64linuxgnunmubootgrepreldynendcutf1d);toolsrelocaterelaubootnodtb。bin0x00000000startend;}{rmfubootnodtb。bin;false;}
start(aarch64linuxgnunmgrepreldynstartcutf1d);end(aarch64linuxgnunmgrepreldynendcutf1d);toolsrelocaterelastartend
cpubootnodtb。binuboot。bin
aarch64linuxgnuobjdumptubootuboot。sym
。。。。
这里的链接器就是aarch64linuxgnuld。bfd。链接脚本就是uboot。lds,用T命令行选项来指定。输入文件就是各个目录下编译好的buildin。o以及lib库。输出文件就是uboot,通过o命令行选项来指定。
千呼万唤始出来,我们来看看uboot。lds这个链接脚本长的帅不帅:
linuxerlinuxervirtualmachine:workuboot2022。01vimuboot。lds
OUTPUTFORMAT(elf64littleaarch64,elf64littleaarch64,elf64littleaarch64)
OUTPUTARCH(aarch64)
ENTRY(start)
SECTIONS
{
。0x00000000;
。ALIGN(8);
。text:
{
(。imagecopystart)
archarmcpuarmv8start。o(。text)
}
。efiruntime:{
efiruntimestart。;
(。text。efiruntime)
(。rodata。efiruntime)
(。data。efiruntime)
efiruntimestop。;
}
。textrest:
{
(。text)
}
。ALIGN(8);
。rodata:{(SORTBYALIGNMENT(SORTBYNAME(。rodata)))}
。ALIGN(8);
。data:{
(。data)
}
。ALIGN(8);
。。;
。ALIGN(8);
。ubootlist:{
KEEP((SORT(。ubootlist)));
}
。ALIGN(8);
。efiruntimerel:{
efiruntimerelstart。;
(。rel。efiruntime)
(。rel。efiruntime。)
linuxerlinuxervirtualmachine:workuboot2022。01catuboot。lds
OUTPUTFORMAT(elf64littleaarch64,elf64littleaarch64,elf64littleaarch64)
OUTPUTARCH(aarch64)
ENTRY(start)
SECTIONS
{
。0x00000000;
。ALIGN(8);
。text:
{
(。imagecopystart)
archarmcpuarmv8start。o(。text)
}
。efiruntime:{
efiruntimestart。;
(。text。efiruntime)
(。rodata。efiruntime)
(。data。efiruntime)
efiruntimestop。;
}
。textrest:
{
(。text)
}
。ALIGN(8);
。rodata:{(SORTBYALIGNMENT(SORTBYNAME(。rodata)))}
。ALIGN(8);
。data:{
(。data)
}
。ALIGN(8);
。。;
。ALIGN(8);
。ubootlist:{
KEEP((SORT(。ubootlist)));
}
。ALIGN(8);
。efiruntimerel:{
efiruntimerelstart。;
(。rel。efiruntime)
(。rel。efiruntime。)
efiruntimerelstop。;
}
。ALIGN(8);
。imagecopyend:
{
(。imagecopyend)
}
。ALIGN(8);
。reldynstart:
{
(。reldynstart)
}
。rela。dyn:{
(。rela)
}
。reldynend:
{
(。reldynend)
}
end。;
。ALIGN(8);
。bssstart:{
KEEP((。bssstart));
}
。bss:{
(。bss)
。ALIGN(8);
}
。bssend:{
KEEP((。bssend));
}
DISCARD:{(。dynsym)}
DISCARD:{(。dynstr)}
DISCARD:{(。dynamic)}
DISCARD:{(。plt)}
DISCARD:{(。interp)}
DISCARD:{(。gnu)}
}
咋一看是不是挺帅的,好,那我们来认识一下这位大帅哥。
他首先是一个文本文件,里面有一系列命令。其中SECTIONS该命令用于描述输出文件的内存布局,它后面跟着花括号中的一些列符号分配和输出段的描述。该命令的第一行设定了特殊符号。的值,。值是位置计数器。如果不用其他方式指定输出段的地址,地址从位置计数器的当前值开始计算。在SECTIONS命令的最开始,位置计数器的值是0。
接下来定义输出段。text。冒号是必需的格式。在输出段的名称后有一个花括号,里面可以列举放入该输出段的输入段。是匹配任何文件名的通配符。表达式(。imagecopystart)表示所有输入文件中的所有。imagecopystart输入段。接下来是表达式archarmcpuarmv8start。o(。text)表示所有输入文件中的。text段的archarmcpuarmv8start。o。
题主的关切点来了:
UBoot执行的第一条指令用链接脚本中的术语叫做入口点。链接脚本中使用ENTRY命令来设置。参数是一个符号名称:ENTRY(symol),这里的符号就是start。这里的start是什么呢?就是0x0,查看方式如下:
那么0x0后面放什么呢?摘取关键部分如下:
。0x00000000;
。ALIGN(8);
。text:
{
(。imagecopystart)
archarmcpuarmv8start。o(。text)
}
0x0后面放text段,text段里面先放什么呢?就是(。imagecopystart),是什么呢,还是0x0:
下面就是主角的真面目了,就是archarmcpuarmv8start。o(。text),再揭开面纱一睹真容:
linuxerlinuxervirtualmachine:workuboot2022。01vimarcharmcpuarmv8start。S
。globlstart
start:
breset
。align3
。。。。
懂了吧,现在知道为啥breset就会放在存储介质的0地址了吧,圆满解答题主的问题。
20220403更新(没想到阅读量已快1千,感谢知友的赞同和追更):
关于链接脚本的入口点的补充:
前面提到,可以使用ENTRY链接脚本命令设置入口点。其实链接器支持多种方式设置入口点,会按照如下优先顺序尝试设定入口点:
。e入口命令行选项。
。链接脚本中的ENTRY(symbol)命令。
。已经定义的目标特定符号的值。通常是start。
。链接脚本段中第一个字节的地址。
。地址0。
光说不练假把式,实验来检验。
实验一:屏蔽链接脚本中的ENTRY(start)和archarmcpuarmv8start。o
重新编译后,查看映射表不再是从breset开始了,直接是armsmcccsmc函数:
lessSystem。map
0000000000000000Tarmsmcccsmc
0000000000000000Tefiruntimestart
0000000000000000Timagecopystart
000000000000002cTarmsmccchvc
0000000000000058Tinvokepscifn
00000000000000d4Tefiresetsystem
0000000000000130Wefigettime
0000000000000138Wefisettime
这种情况直接走顺序4,而。text段描述并没有指定具体内容,顺着肯定就是。efiruntime段的第一个字节了。
实验二:保留ENTRY(start),并将archarmcpuarmv8start。o替换为(。text):
这种情况扔能通过ENTRY(start)找到对应入口:
lessSystem。map
0000000000000000Timagecopystart
0000000000000000Tstart
0000000000000008TTEXTBASE
0000000000000010Tendofs
0000000000000018Tbssstartofs
0000000000000020Tbssendofs
0000000000000028treset
000000000000002cTsavebootparamsret
但无法直接看出是哪个。o,实际确实是archarmcpuarmv8start。o,因为只有archarmcpuarmv8start。S定义了。globlstart。
这种情况直接走顺序3。
实验三:删除ENTRY(start),并将archarmcpuarmv8start。o替换为(。text):
扔能找到archarmcpuarmv8start。S作为入口:
lessSystem。map
0000000000000000Timagecopystart
0000000000000000Tstart
0000000000000008TTEXTBASE
0000000000000010Tendofs
0000000000000018Tbssstartofs
0000000000000020Tbssendofs
0000000000000028treset
000000000000002cTsavebootparamsret
这种情况直接走顺序4。
剩下的实验就交给你啦。