翼度科技»论坛 云主机 LINUX 查看内容

重定位

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
重定位相关的几个重要概念:


  • 加载地址:存储代码的物理地址,在GNU链接脚本里称为LMA。例如,ARM64处理器上电复位后是从异常向量表开始取第一条指令的,所以通常这个地方存放代码最开始的部分,如异常向量表的处理代码
  • 运行地址:程序运行时的地址,在GNU链接脚本里,称为VMA,即虚拟地址
  • 链接地址:在编译,链接时指定的地址,程序员设想将来程序要运行的地址。程序中所有的标号的地址在链接后就确定了,不管程序在哪里运行都不会改变。objdump工具可以进行反汇编,查看的就是链接地址。
链接脚本的输出段格式描述:
链接脚本输出段格式
  1. section [address] [(type)] :
  2.         [AT(lma)]
  3.         [ALIGN(section_align)]
  4.         [constraint]
  5.         {
  6.                 output-section-command
  7.                 output-section-command
  8.                 ...
  9.         } [>region] [AT>lam_regin] [:phdr :phdr ...] [=fillexp]
  10.        
复制代码
解析:


  • section: 段的名字,例如代码段.text,数据段.data等
  • address: 虚拟地址
  • type: 输出段的属性
  • lma:加载地址
  • ALIGN:对齐要求
  • output-section-command:描述输入端如何映射到输出段
  • region:特定的内存区域
  • phdr:特定的程序段
一个输出段有两个地址:VA和LMA,分别是虚拟地址和加载器地址
虚拟存储地址,是运行时段所在的地址,可以理解为运行地址。
加载存储器地址是加载时段所在的地址,可以理解为加载地址。
如果没有用AT显式指定LMA,那么LMA=VA,加载地址等于虚拟地址。
嵌入式系统中,加载地址经常与虚拟地址不一致,比如映像文件载入到开发板的flash存储中,而bootloader又会将flash中的映像文件复制到SDRAM中(由VA指定)
例子:
通常,为了构建一个基于ROM的映像文件,要设置输出段的虚拟地址和加载地址不一致。映像文件存储在ROM中,运行时需要把映像文件拷贝到RAM中,此时,ROM中的地址就是加载地址,而RAM中的地址就是虚拟地址,即运行地址
一段实例链接脚本
  1. SECTIONS
  2. {
  3.         .text 0x1000:
  4.         {
  5.                 *(.text) _etext = .;
  6.         }
  7.         .mdata 0x2000:
  8.         AT (ADDR (.text) + SIZEOF(.text) )
  9.         {
  10.                 _data = .;
  11.                 *(.data);
  12.                 _edata = .;
  13.         }
  14.         .bss 0x3000:
  15.         {
  16.                 _bstart = .;
  17.                 *(.bss) *(COMMON);
  18.                 _bend = .;
  19.         }
  20. }
复制代码
这里创建了三个段:
代码段的虚拟地址和加载地址都是0x1000
mdata段为用户自定义的数据段, 虚拟地址为0x2000,而加载地址为代码段的结尾位置的地址
符号_data的值为0x2000
bss段的虚拟地址为0x3000,加载地址没有指定,所以和加载地址一致。
由于mdata段的虚拟地址和加载地址不一致,所以程序的初始化代码应该把.mdata段从ROM中加载地址处复制到SDRAM中的虚拟地址处。
点击查看代码
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4. extern unsigned long _etext;
  5. extern unsigned long _data;
  6. extern unsigned long _edata;
  7. extern unsigned long _bstart;
  8. extern unsigned long _bend;
  9. unsigned long *src = &_etext;
  10. unsigned long *dst = &_data;
  11. unsigned long *src_end = &_edata;
  12. unsigned long *bss_start = &_bstart;
  13. unsigned long *bss_end = &_bend;
  14. /*
  15. * @brief: copy mdata contain from rom space to sdram space
  16. * @args: none
  17. * @ret:
  18. *      non-zero if work abnormally
  19. *      zero if work successfully
  20. *
  21. * */
  22. int load_mdata_from_rom_to_sdram() {
  23.   if (src == NULL || dst == NULL) {
  24.     return -1;
  25.   }
  26.   if (src == dst) {
  27.     return 0;
  28.   }
  29.   while (dst < src_end)
  30.     *dst++ = *src++;
  31.   return 0;
  32. }
  33. /*
  34. * @brief: init bss section by fill this area with 0
  35. * @args: none
  36. * @ret:
  37. *       non-zero if init failed
  38. *       zero if init ok
  39. */
  40. int bss_init() {
  41.   for (unsigned long *cur = bss_start; cur < bss_end; cur++) {
  42.     *cur = 0;
  43.   }
  44.   return 0;
  45. }
  46. int main() {
  47.   load_mdata_from_rom_to_sdram();
  48.   bss_init();
  49.   return 0;
  50. }
复制代码
链接地址和运行地址何时不同,何时相同

链接地址等于运行地址和加载地址

树莓派4B上电以后,首先运行芯片内部的固件(引导程序bootcode.bin和GPU的固件start4.elf),然后把os.bin加载到0x80000地址处,并且跳转过去执行。
点击查看代码
  1. SECTIONS
  2. {
  3.         . = 0x80000;
  4.         .text.boot : { *(text.boot) }
  5.         .text : { *(.text) }
  6.         .rodata : { *(.rodata) }
  7.         .data : { *(.data) }
  8.         . = ALIGH(0x8)
  9.         bss_begin = .;
  10.         .bss : { *(.bss) }
  11.         bss_end = .;
  12. }
复制代码
在这个案例中,链接地址是0x80000开始,此时加载地址也是0x80000, 运行地址也是从0x80000开始的。
链接地址 不等于 加载地址

点击查看代码
  1. TEXT_ROM = 0x90000;
  2. SECTIONS
  3. {
  4.         . = 0x80000,
  5.        
  6.         _text_boot = .;
  7.         .text.boot : { *(.text.boot) }
  8.         _etext_boot = .;
  9.        
  10.         _text = .;
  11.         .text : AT(TEXT_ROM)
  12.         {
  13.                 *(.text)
  14.         }
  15.         _etext = .;
  16. }
复制代码
在13行使用AT表面代码段的加载地址为0x90000,这样代码段的加载地址和链接地址就不一样。,但是链接地址和运行地址一样(都是_text符号表示的地址)。如果通过objdump查询链接地址,可能发现text所在位置为0x80088, 但是load address在0x90000
这种情况下,如果想要正常运行os.bin的代码,需要把代码段从加载地址复制到链接地址
链接地址 不等于 运行地址

使能OS的MMU之后,可以将DDR内存,映射到操作系统内核空间。
案例脚本:
点击查看代码
  1. OUTPUT_ARCH(aarch64)
  2. ENTRY(_start)
  3. SECTIONS
  4. {
  5.         . = 0xffff000010080000;
  6.         _text_boot = .;
  7.         .text.boot : {
  8.                 *(.text.boot)
  9.         }
  10.        
  11.         _etext_boot = .;
  12.         _text = .;
  13.         .text : {
  14.         *(.text)
  15.         }
  16.         _etext = .;
  17.         ...
  18. }
复制代码
第六行首先使用位置计数器,把代码段的链接地址设置为0xffff000010080000,这是内核空间的一个地址(真实DDR可能没有这么大的地址空间,通过MMU提供了一个4G大小的虚拟地址空间)。树莓派4B上电后,首先会让os.bin加载到0x80000地址处。此时运行地址和加载地址都是0x80000,但是链接地址是0xffff000010080000。
在初始化代码中,需要初始化MMU,建立页表映射,把DDR内存映射到内核地址空间,然后做一次重定位操作,让CPU的运行地址重定位到链接地址上,这样就可以用链接地址,经过页表的映射,找到真实的DDR地址。这在uboot和linux中是很常见的操作。
重定位

假设有一块开发板,芯片内部有SRAM,其实地址为0x0,DDR地址为0x40000000。
一般来说代码会存储在Nor Flash或Nand Flash中,芯片内部的bootrom会把开始的小部分代码,load到SRAM中,然后从SRAM中取值执行。由于Uboot镜像太大,SRAM不够存,所以Uboot镜像必须放在DDR内存里。
通常来说编译Uboot时链接地址都会设置到DDR内存中,也就是0x40000000地址处。运行地址和链接地址就不一样了,运行地址是SRAM的0X0,而链接地址是DDR的0x40000000,程序还能运行吗?
可以运行,只不过需要位置无关码的支持。
位置无关码和位置有关码,是指一部分是否和内存地址有关的代码。位置无关码是与内存地址无关的代码,无论运行地址是否与链接地址相等,这种代码都可以正常执行。汇编指令BL, B, MOV等指令,就是位置无关指令,无论加载到哪个位置,都可以正常运行
位置有关码是和内存地址有关的代码,这种代码一般涉及的地址,都是决定地址,和当前的PC值无关。ARM汇编里面通过绝对跳转指令修改PC值为当前的链接地址的值:
ldr pc, =on_sram @跳转到SDRAM上继续执行
这种通过LDR指令跳转到链接地址并执行的操作,让运行地址等于链接地址,就被叫做重定位。程序被重定位之前只能执行一些位置无关码。
为什么要刻意地设置加载地址,运行地址,以及链接地址不一样?

如果所有的代码都在ROM中执行,那么运行地址和链接地址与加载地址都是同一个地址。但现实是很多硬件上,都会把程序加载到DDR内存中运行,DDR的访问速度比ROM快得多,而且容量也很大,所以设置链接地址在DDR内存中,而程序的加载地址在ROM中,这两个地址是不相同的。
如何让程序能在链接地址上运行呢?

程序的加载地址等于ROM的基地址(或者在SRAM上),而链接地址等于DDR内存中某一处的起始地址(暂且称为ram_start)。
程序先从ROM或者SRAM启动,这块代码中要实现程序复制功能(把整个rom代码段复制到ddr上),然后通过LDR指令跳转到DDR内存上,也就是在链接地址上执行了(B指令无法实现这个跳转,因为它是PC相关的)。
上述重定位过程在uboot中实现,以下是概览图

Uboot需要引导linux内核,则需要把Linux内核Image复制到DDR内存中,然后跳转到内核入口地址处(stext)。
当跳转到内核入口地址stext函数时,程序运行在运行地址上,即DDR内存的地址。但是我们从vmlinux中看到stext函数的链接地址是虚拟地址。内核启动汇编代码也需要一个重定位过程。这个重定位过程在__primarty_switch函数中完成,__primay_switch汇编函数的主要功能是初始化MMU和实现重定位。
启动MMU之后,通过LDR指令把__primary_switched函数的链接地址加载到x8寄存器中,通过BR指令跳转到__primary_switched函数的链接地址处,从而实现重定位。
点击查看代码
  1. // <linux5.0/arch/arm64/kernel/head.S>
  2. __primary_switch:
  3.         adrp        x1, init_pg_dir
  4.         bl                __enable_mmu        //开启MMU后,CPU用的是虚拟地址,所以需要重定位
  5.        
  6.         ldr                x8, =__primary_switched                //重定位
  7.         adrp        x0,        __PHYS_OFFSET
  8.         br                x8
复制代码


来源:https://www.cnblogs.com/ryanos/p/17964384
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具