卢骏 · 2020年07月03日

CORTEX-A8裸机系列:第七章 cache和代码重定位

第七章 cache和代码重定位

本章代码,查看我的github网址:https://github.com/weiqi7777/...

1、cache

Cache是一种内存,叫高速缓存。

Cache的存在,是因为寄存器和DDR之间速度差异太大,DDR的速度远不能满足寄存器的需要。Cache的本质,还是一片内存,只不过是访问速度很快的SRAM。

210内部有32KB icache和32KB dcache(传说中的哈弗结构,指令和数据是分开)。Icache是用来缓存指令的;dcache用来缓存数据。

Cache的意义:指令平时是放在硬盘/flash中,运行时读取到DDR中,再从DDR中读给寄存器,再由寄存器送给CPU。但是DDR速度和寄存器的速度相差太大,如果CPU读取一次数据,处理完毕后,在去取下一个数据,那么CPU的速度完全被DDR拖慢了。因此为了解决这个问题,就利用了cache。

Cache分为两类:

◾icache:缓存指令使用,icache工作时,会把CPU正在运行的指令,以及附近的指令事先给读取保存到icache中。当CPU需要指令时,icache首先检查自己事先准备的缓存指令中有没有该指令,如果有就直接给CPU,如果没有的话,需要从DDR中重新读取拿给CPU,并同时做一系列的动作:清缓存,重新缓存(refill)。

◾dcache:缓存数据使用。Dcache工作时,会把CPU从内存中取的数据,以及附近的数据都取出来保存在dcache中。当CPU要访问数据时,dcache判断该数据是否在保存在自己内部,如果存在,就是命中(hit),然后CPU直接访问这个数据。如果没有保存在自己内部,就是丢失(miss),那么dcache就要从内存中,将该数据取出来保存在自己内部,CPU在来访问dcache。

Cache的一切动作都是自动的,不需要人为干预,即这些对于软件编程人员,都是透明的。我们所需要做的就是打开/关闭cache。

在裸机开发时,一般icache是打开的,dcache是关闭的。下面只讨论icache。

IROM的BL0中,有对icache进行了初始化,也就是打开了icache。Icache和协处理器有关,由CP15的C1寄存器控制
1.png

由第12位控制。0关闭,1开启。因此将这一位置1,就表示将icache开启了。
2.png

2、代码重定位
2.1、位置无关编码(PIC)和位置有关编码。

◾位置无关编码(PIC):汇编源文件被编码成二进制可执行程序时,编码方式与位置(内存地址)无关

◾位置有关编码:汇编源文件被编码成二进制可执行程序时,编码方式与位置(内存地址)有关

对于位置有关代码,最终执行时的运行地址和编译链接时的地址必须相同,否则会出错。

位置无关代码要好一些,适应性强,放在哪里都能正常运行。

位置有关代码就必须运行在链接时指定的地址上,适应性差。

位置无关代码有一些限制,不能完成所有功能,有时候不得不用位置有关代码。

2.2、链接地址和运行地址

◾链接地址:链接时指定的地址(指定方式:makefile中用-Ttext,或者链接脚本)

◾运行地址:程序实际运行时地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)

Makefile中使用-Ttext 0x0, 来指定链接地址是0x0。这意味着程序将来会放在0x0这个内存地址去运行。但是实际上运行时的地址是0xd0020010(dnw下载的地址)。所以0x0是链接地址,0xd0020010是运行时地址。

2.3、bootloader的启动方式

三星推荐的启动方式:bootloader必须小于96K并大于16K。假定bootloader为80K,启动过程以下:

开机上电后BL0执行,BL0加载外部启动设备中的bootloader的前16K(BL1)到SRAM中运行,BL1运行时会加载BL2(bootloader中80-16=64K)到SRAM中(从SRAM中的16K开始)去执行;BL2运行时会初始化DDR并且将OS搬运到DDR中去执行OS,启动完成。

Uboot实际使用的启动方式:uboot大小随意,假定为200K。启动过程是:开机上电BL0运行,BL0会加载外部启动设备中的uboot的前 16K(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中发,然后用一句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot直到uboot完全启动。Uboot启动后再去启动kernel。

2.4、分散加载

分散加载:把uboot分成2部分(BL1和整个uboot),两个部分分别指定不同的链接地址,启动时,将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR)。这个时候不用重定位也能启动。

分散加载其实相当于手工重定位。重定位是用代码来进行重定位,分散加载时手工操作重定位的。

2.5、可执行程序的生成

Linux中的应用程序、 gcc hello.c –o hello

这时候使用的是默认的链接地址0x0,所以应用程序都是链接在0地址的。因为应用程序运行在操作系统的一个进程中,在这个进程中这个应用程序独享4G的虚拟地址空间。所以应用程序都可以链接到0地址,因为每个进程都是从0地址开始的。但是注意,这个0地址是虚拟地址。

源码到可执行程序的步骤:

1) 预编译:预编译器执行。比如C中的宏定义,注释等。

2) 编译: 编译器来执行。将源码编译成机器码.o

3) 链接: 连接器来执行,把.o文件中的各个段按照一定规则(由链接脚本来指定)结合在一起,形成可执行文件(elf文件)。

4) Strip: strip是把可执行程序中的符号信息拿掉,以节省空间。(Debug版本和Release版本)

5) Objcopy:将可执行程序(elf文件)生成可烧录的镜像bin文件。

1-3是必选的,4-5是可选的。

2.6、段

段就是程序的一部分,我们把整个程序的所有东西分成了一个一个的段,给每个段起个名字,然后在链接时就可以用这个名字来指示这些段。也就是说给段命名就是为了在链接脚本中用段名来让段链接在合适的内存地址处。

段名分两种:一种是编译器连接器内部定好的,先天性的名字;一种是程序员自己指定的、后天性的段名。

先天性段名:

◾代码段:(.text), 又叫文件段,代码段其实就是代码编译后生成的东西

◾数据段:(.data), 数据段就是代码中有初始化为非0的全局变量或静态变量

◾Bss段:(.bss), 又叫ZI段(zero initial)段,就是零初始化段,对应代码中初始化为0的全局变量或静态变量

后天性段名:

段名由程序员自己定义,段的属性和特征也由程序员自己定义。在bootloader中就定义了很多的自定义段。

一些问题:

1) C语言中全局变量如果未显示初始化,值是0。本质就是C语言把这类全局变量放在了BSS段,从而保证了为0

2) C运行环境如何显示初始化为非0的全局变量的值在main之前就被赋值了?就是把这类变量放在了.data段中,而.data段会在main执行之前被处理(初始化)。

2.7、链接脚本

链接脚本其实是个规则文件,他是程序员用来指挥链接器工作的。链接器会参考链接脚本,并且使用其中规定的规则来处理.o文件中的那些段,将其链接成一个可执行程序。

链接脚本的关键内容:

1) 段名: 指示内容

2) 地址(作为链接地址的内存地址):把段给链接到什么地址

链接脚本的理解:

SECTIONS {} 整个链接脚本

. 点号在链接脚本中表示当前地址 . = 给当前地址赋值,注意.和=之间要有空格

= 等号代表赋值

.text :{} 表示代码段,注意 .text和:之间也有空格。

image.png

通过链接脚本,可以指定各个.o的位置顺序,以及链接的地址。还可以获取到bss段的起始和结束地址。

2.8、ldr和adr区别

ldr和adr都是伪指令,区别是ldr是长加载,adr是短加载

重点:adr指令加载符号地址时,加载的是运行时地址;ldr指令在加载符号地址时,加载的是链接地址。

深入分析:

只要知道adr和ldr分别加载运行地址和链接地址,从而可以判断是否需要重定位即可。

代码:
3.png

对应的反汇编代码:
4.png

可以看到,adr指令被sub指令所替代,r0的值为当前pc的值减去36,当前pc值是0xd002_4024,减去36,刚好为0xd002_4000。是_start的运行时地址。

ldr r1, =_start。 将_start的链接地址保存到了一个地址空间,从该空间取出数据在给r1寄存器。

系列其他篇

原文首发于骏的世界博客
作者:卢骏.
更多Arm技术相关的文章请关注Arm技术博客极术专栏,每日更新。
推荐阅读
关注数
23386
内容数
892
Arm相关的技术博客,提供最新Arm技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息