重定位和链接脚本
大部分指令是“位置有关编码”
- 位置无关编码:汇编源文件编码成二进制可执行程序时,编码方式与位置无关。
在我们写程序时,必须给编译器链接器指定地址。将来的程序被执行时必须放在当时编译链接时给定的地址才能运行。
- 位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的。
但是也有一种特别的指令他可以跟指定的链接地址没有关系,这些代码不管放在哪里都可以正常运行。
分析:
之前的裸机程序中,makefile中用-Ttext 0x0来指定链接地址是0x0;这意味着我们认为将来这个程序会被放在0x0这个地址去运行。但是实际上我们在DNW中,把程序下载到0xd0020010. 在s5pv210中,由于在内部做了映射,所以0xd002_0010和0x0000_0010是一样的。那么为什么放在0x0而不是0x0000_0010依旧可以呢?原因就这个是位置无关编码。
- 链接地址:链接时指定的地址(指定方式:makefile中用T-text,或者链接脚本)
- 运行地址:程序实际运行的地址(指定方式:由实际运行时被加载到内存那个位置说了算)
在linux中的应用程序:gcc hello.c -o hello
- 这时候默认的链接地址就是0x0,所以链接在0地址,因为应用程序运行在操作系统的一个进程中,这个进程独享了4G的内存空间,所以应用程序可以链接到0地址,因为每个进程都是从0地址开始的。
- 210中的裸机程序运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址是iROM中的BL0加载BL1时实现指定好的地址,这个是由CPU设计时候决定的)。所以理论上我们编译链接时应该指定到0xd0020010,但是实际上我们之前的裸机程序都是使用位置无关码PIC,所以链接地址可以是0
再分析s5pv210的启动过程
三星推荐的启动方式:
bootloader必须大于16KB小于96KB,假定为80KB。启动过程如下:开机上电后BL0运行,BL0加载外部启动设备中的bootloader的前16KB(BL1)到SRAM中运行,BL1运行后会加载BL2到(80-16=64KB)到SRAM中运行,BL2运行时会初始化DDR并且将OS搬到DDR中执行,启动完成。
uboot实际使用方式:
uboot大小随意,假定为200KB,启动过程:先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB到SRAM运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用一句长跳转指令从SRAM直接跳转到DDR中继续执行uboot直到uboot完全启动。uboot启动后在命令行中启动OS
为什么需要重定位?
- 原因:链接地址和运行地址有时候必须不相同,而且不能全部用位置无关码,这种就只能重定位
- 扩展:分散加载:把uboot分为2部分(BL1和整个uboot)两个部分分别制定不同的链接地址。启动时将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR),这时候不用重定位也能启动
- 评价:分散加载其实相当于手工重定位,重定位是用代码来重定位,分散加载是手工操作重定位
从源码到可执行程序的步骤:
- 预编译:预编译器执行。譬如C中的宏定义就是由预编译处理器处理,注释等也是由预编译器处理的
- 编译:编译器来执行。把源码.c .S编译成机器码
- 链接:连接器来执行。把.o文件中的各个程序按照一定的规则(链接脚本指定)累积在一起
strip:strip是把可执行程序中的符号给拿掉,以节省空间(debug版本和release版本)
objcopy:由可执行程序生成可烧录的镜像bin文件
程序段的概念:代码段、数据段、bss段(ZI段)、自定义段
段是程序的一部分,我们把整个程序的所有东西分成了一个个段,给每个段起一个名字,然后再链接时可以用和这个名字来指示这些段
段名分为2种:一种是编译器连接器内部定好的,一种是程序员自己指定的,自定义的段名
先天性段名:
- 代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
- 数据段:(.data),数据段就是C语言中显示初始化为非0的全局变量
- bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量
后天性段名:
段名由程序员自己定义,段的属性和特征也由程序员自己定义。
分析一些问题,跟这里结合,然后试图明白一些本质
- C语言中全局变量如果未显示初始化,值为0。本质上是因为C语言中我们将这类全局变量放在了bss段,从而保证了为0
- C运行时环境如何保证显示初始化为非0的变量在main函数之前就被赋值了,就是因为把这类变量放在了.data段了,而.data段会在main函数之前被处理。
链接脚本就是一个规则文件,他是程序员用来指挥链接器用来工作的。链接器会参考链接脚本并且使用其中规定的规则来处理.o文件中的那些段,将其链接成可执行程序。
链接脚本的关键内容有两个部分:段名+地址(作为链接地址的内存地址)
链接脚本的理解:
SECTIONS{}这个是整体链接脚本
. 点号在链接脚本中代表当前位置
= 等号代表赋值
SECTIONS { . = 0xd0024000; .text : { start.o *(.text) } .data : { *(.data) } bss_start = .; .bss : { *(.bss) } bss_end = .; }
举例分析
- 第一点:通过链接脚本将代码链接到0xd00240000
- 第二点:dnw下载时将bin文件下载到0xd0020010
这两点就保证了:代码实际下载运行在0xd0020010,但是却被链接在0xd0024000,从而为从定位奠定了基础。
我们把链接地址设置为0xd0024000时,隐含意思就是这段代码将来必须放在0xd0024000位置才能正确执行。
如果实际运行地址不是这个地址就要出事(除非代码是位置无关码)。所以重定位代码的作用就是在PIC执行前(代码中第一句位置有关码执行前)必须将整个代码搬移到0xd0024000去执行。
- 第三点:代码执行时通过代码前段的少量位置无关码将整个代码搬移到0xd0024000
- 第四点:使用一个长跳转跳转到0xd0024000处的代码处继续执行,重定位完成
长跳转:首先这局代码是一句跳转指令(ARM的跳转指令就是类似于分支指令B、BL等作用指令),跳转指令通过pc(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转到的地址和当前地址差异比较大,跳转的范围比较宽广。
当我们把执行完代码重定位后,实际上在SRAM中有两份代码的镜像(一份是我们下载到0xd0020010处的开头,另一份是重定位代码复制到0xd0024000处开头的),这两份内容完全一样,仅仅是地址不同。重定位之后使用ldr pc, =led_blink这局长跳转指令直接从0xd0020010跳转到0xd0024000开头的那份代码段的led_blink的函数处去执行led_blink。如果短跳转bl led_blink则会跳转到0xd0020010开头的那一份led_blink.