04.链接 & 重定位
一、编译之后的操作是链接,编译的顺序依照 makefile 里的文件顺序。SOC中可以使用ld命令指定,一般使用链接脚本。(http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html)
1. 链接脚本是个规则文件,用来指挥链接器工作,扩展名位lds。主要用来描述如何把输入文件中的节(sections)映射到输出文件中,并控制输出文件的存储布局。链接器参考链接脚本中的规则,并将其链接成可执行的程序(UNIX或GNU/Linux下, 一般为ELF格式)。
其描述在汇编指令中有说明2. 链接先后顺序,默认以makefile里面文件的先后顺序做依据。否则先后顺序由链接脚本指定。
3. Linux应用程序,运行时默认的链接地址为0x0。它运行在操作系统的一个进程中,此进程独享4G的内存栈空间。Linux中每个进程都是从0地址开始,所以应用程序可以链接到0地址。
4. 裸机中程序链接地址为内存集合中的地址,一般不为0。程序运行地址由下载时指定(由CPU设计候决定)。编译链接时应该指定地址。如果使用位置无关码,链接地址可以是0。
5. 加载器会把可执行程序加载到内存的运行地址运行。裸板的加载器是JTAG调试工具,但JTAG等调试器一般只支持一体式链接脚本。Linux中应用程序加载器也是应用程序。
6. 操作流程:①设置入口函数 ==> ②定义一个变量并赋值 ==> ③描述输入输出文件的链接规则。
①. 对于arm-gcc-ld脚本,设置入口函数和定义变量可以在SECTIONS命令大括号里,也可以在外面。下列入口定义方式优先级递减:1和2显示的指明入口(推荐)。4和5,有.text段的话就从它开始执行,连.text都没有的就从0地址开始执行。
1️⃣. 在连接的时候使用-e参数。
2️⃣. 在脚本里使用ENTRY(begin)。begin指定程序的入口点为begin标记。
3️⃣. (汇编文件里)定义过的start入口。
4️⃣. SECTION中.text的第一个入口函数。
5️⃣. 地址为0的指令。
②. 提供变量并使用伪汇编指令赋值。其值为汇编代码的启动地址。例如" .= 0x30000000; "。必须注意,链接脚本中对地址的赋值语句末尾需要添加" ; "分号。
③. 描述输入输出文件的链接规则:" SECTIONS "和 " MEMORY "。" SECTIONS "描述输出文件的布局,不可缺省。" MEMORY "补充SECTIONS。如果缺省,LD会认为" SECTIONS "描述的相邻内存块之间有足够的可用空间。SECTIONS中分布的段,不会考虑寻址范围里的ROM、RAM、FLASH是否连续。如果不连续,MEMORY命令就设置各个区的起始位置、大小、属性。一个脚本中只能有一个属性。
SECTIONS 格式 MEMORY 格式7. 变量有2种赋值方式:直接赋值(_start、_end等)。取址赋值(用链接脚本手册里的函数获取地址。例如LOADADDR等)。其用于之后的代码重定位。使用C语言取得变量的地址时
二、两种链接脚本:
1. 分体链接脚本适合单片机。单片机自带flash,不需要再将代码复制到内存占用空间。嵌入式系统内存非常大,没必要节省这点空间。并且有些嵌入式系统没有可以直接运行代码的Flash(Nor Flash等),需要从Nand Flash或者SD卡复制整个代码到内存。
2. JTAG等调试器一般只支持一体式链接脚本。
两种链接脚本三、重定位
1.大部分程序是从存储介质拷贝到内存中执行 - 重定位。链接脚本指定和获取程序各段地址,供程序拷贝数据段,和清除bss段。重定位完毕后,需要用 ldr 跳转到 sdram 中执行代码。
2. 重定位分位静态重定位和动态重定位。
静态:在程序执行前重定位。根据装配模块将要装入的内存起始位置,直接修改装配模块的有关使用地址的指令。由代码段实现。
动态:在程序执行时重定位。访问内存单元前才变换地址。装配模块不加任何修改的装入内存,但需要定位寄存器的指出。
3. uboot启动分为2部分,分别依次加载到内存运行。第一部分是拷贝uboot前面一段到SRAM中运行,第二部分初始化DDR并将uboot搬到DDR中执行。程序的链接地址和运行地址会发生改变,但代码中又存在位置有关码。需要进行代码重定位。(例如裸机中,DRAM还未进行初始化,但代码又必须在DRAM上运行)。
4. 汇编代码分为位置无关码和位置有关码。
位置无关码:运行时与代码在内存中的地址无关。代码使用相对地址而非绝对地址。(使用相对跳转指令B、BL、MOV等)
位置有关码:链接地址和运行地址必须一致。(ldr r1, = xxx等指令)。
运行地址:代码执行时的地址(指定方式:实际运行时被加载到内存的地址)。
链接地址:链接时指定的地址(指定方式:makefile中用T-text,或链接脚本)。
5. 重定位copy完代码后,某些代码会存在2份。重定位后如果想在SDRAM上运行代码(runtime address),必须用ldr指定绝对地址(ldr pc = xxx)。相对跳转则只会跳转到pc + offset的地址(仍然运行在SRAM或者nor flash上)。重定位前的代码必须使用相对地址跳转指令,不能使用绝对地址。
6. 全局变量、静态变量、有初始值的数组,其读取时的跳转指令在反汇编中显示为ldr而不是b或者bl,属于绝对地址。必须在重定位之后访问。
7. 必须注意,重定位之前一定要初始化完毕 sdram,否则呵呵。。。
四、如何重定位
1、Nand Flash 启动步骤:
①、硬件复制 Nand Flash 前面一段内容到片内 SRAM。
②、从片内 RAM 运行,其基地址为0。
③、此状态下 Nor Flash 不可访问。
④、程序如果大于 SRAM,则前面一段拷贝到SRAM中的代码需要把整个程序读出来放到SDRAM(即代码重定位)。
重定位方法:重定位数据段和代码段。通过链接脚本等,让文件链接到SDRAM起始位置,全局变量紧随其后。烧写到flash的0地址处。上电后程序会把代码段数据段(整个程序)从0地址复制到SDRAM的起始地址。
2. Nor Flash 启动步骤:
运行时区域图①、Nor Flash 基地址为0。
②、CPU从 Nor Flash 上读出指令执行。
③、内存(4K RAM)基地址为 0x40000000(2440为例)。
④、把全局变量和静态变量重定位到 SDRAM 里。
⑤、初始化bss段。全部清零。
重定位方法一:和nand flash相同。因为SDRAM较大,所以常用此方法。
重定位方法二:仅重定位数据段。通过链接脚本等,让数据段链接到代码段之后。烧写运行时会把数据段复制到SDRAM的起始地址。
Ⅰ、链接脚本指定链接地址:设定程序运行的起始地址、各段的起始和链接地址、供后续程序使用的地址变量。
Ⅱ、代码的重定位拷贝:判断运行地址和链接地址,手动编写代码拷贝程序(复制代码到指定的链接地址处)。
Ⅲ、初始化变量:从链接脚本中,读取bss段运行时的起始和结束地址,手动清零。
Ⅳ、使用长转移指令跳转到SDRAM上运行。(ldr pc, = main)
3. 以方法一为例,实现copy和clean:
拷贝代码 清除bss段4. 注意,用C语言从链接脚本中获取地址时,为了规范必须类型转换,且要加上取址符号" & "。C语言使用"extern"声明外部变量时,会在编译该文件生成的符号表"symtab"中生成对应的存储。类似键值对(name:xxx -- addr:xxx)。从链接脚本中获取到的变量是一个值(不是地址),没有在内存中对应的值,所以需要手动添加取址符号来取出其值。