【韦东山旧1期学习笔记】08.S3C2440 代码重定位实验

实验环境

        本实验基于这样的开发环境:在Ubuntu18.04.3发行版上使用编译器arm-linux-gcc(arm-none-linux-gnueabi, 4.3.2 Sourcery G++ Lite 2008q3-72)编译,并运行于S3C2440 ARM硬件平台上。S3C2440是一款32位的ARM SoC。

基础概念

链接(linking)

        所谓链接,就是将各种代码和数据部分收集起来并组合成一个单一文件的过程,这个单一文件可以被加载到存储器内并被执行。链接既可以执行于编译时,也可以执行于加载时,也可以执行于运行时。若执行于编译时,则是由编译器在将源代码翻译成机器码时负责;若执行于加载时,则是由加载器(loader)把可执行程序加载到存储器中时负责;若执行于运行时,则是由应用程序负责。通常情况下,链接是由链接器程序负责自动执行的。

目标文件

目标文件有三种类型:

  • 可重定位目标文件
  • 可执行目标文件
  • 共享目标文件

其中编译器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。

        在不同的操作系统上,目标文件的格式都不相同。最早从贝尔实验室诞生的第一个Unix系统使用的是a.out格式,所以直到今天,gcc默认生成的可执行文件的名字仍是a.out文件。System V Unix系统的早期版本使用的格式叫做一般目标文件格式(Common Object File Format,COFF)。Windows NT使用的就是COFF的一个变种,叫做可移植可执行(Portable Executable,PE)格式。而现代Unix系统(如Linux,还有System V Unix的后续版本,各种BSD Unix,以及Sun Solaris等系统),使用的是Unix可执行和可链接(Executable and Linkable Format,ELF)格式。
        本实验只讨论ELF格式,但原理在本质上是相通的。下面开始介绍ELF格式文件。

ELF可重定位目标文件

【韦东山旧1期学习笔记】08.S3C2440 代码重定位实验
上图展示的是一个典型的ELF可重定位目标文件的格式。

ELF头、节头部表

        其中第一项的ELF头,是一个16字节的序列。该序列描述了生成该文件的系统的字的大小和字节顺序。而序列剩下的部分则包含了帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者是共享的)、机器类型(如IA32)、节头部表(section header table)的文件偏移,以及节头部表中的条目大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每一个节都有一个固定大小的条目(entry)。
        在ELF头和节头部表之间的部分,都是节(section)。一个典型的ELF可重定位目标文件包括下面要介绍的几个节。
.text
该节是已编译程序的二进制机器代码。
.rodata
只读数据。比如printf语句中的格式字符串和switch语句的跳转表。
.data
数据段。存储的是已初始化过的全局变量。注意,对于C语言的局部变量,在运行时保存在栈中,它既不出现在.data段中,也不出现在.bss段中。
.bss
未初始化的全局变量或者初始化为0的全局变量。在目标文件中,.bss段并不占用实际的空间,只保存描述信息,仅作为一个占位符。未初始化或初始化为0的全局变量不需要占据任何实际的磁盘空间。
.symtab
符号表。存放程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为,只有在编译时使用了-g选项后,目标文件中才会得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表。注意,这里和编译器中的符号表不同,.symtab中不包含局部变量的条目。
.rel.text
这是一个.text段 位置的列表。当链接器把这个目标文件和其他文件结合在一起时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。需要强调的一点是,对于可执行目标文件,其中并不需要重定位信息,因此通常会省略。
.rel.data
被模块引用或定义的任何全局变量的重定位信息。
.debug
调试符号表。只有在编译时添加-g选项才会得到这张表。
.line
原始C源代码的行号,以及.text段中机器指令之间的映射关系。同样,只有在编译时使用-g选项才会得到这张表。

S3C2440地址映射图

【韦东山旧1期学习笔记】08.S3C2440 代码重定位实验

s3c2440的NandFlash启动流程

        通常情况下,Nor Flash相对较昂贵,而Nand Flash和SDRAM则相加比较高。所以多数情况下,我们总是使用Nand Flash来存储代码并用来执行启动代码,同时使用SDRAM作为其余代码的执行地方。
        我们知道,Nand Flash并不支持CPU直接访问。为了支持Nand方式启动,S3C2440在SoC芯片内部内置了一块大小为4KB的SRAM,并将这块SRAM起名叫做起步石(SteppingStone)。当以Nand方式启动时,硬件在复位过程中,会自动将Nand Flash前4KB的数据拷贝到起步石中,复位结束后,CPU会跳转到0x0地址开始执行代码。
        由上图可知,当我们将SoC的OM1和OM0两个管脚置为0x00时,SoC就会以Nand方式启动。此时0x0地址对应的正是起步石SRAM。所以此时就是从起步石开始执行代码。而这4KB的代码,我们就称之为boot code。
        Boot Code要做的工作,就是初始化必要的硬件,并将Nand Flash中剩余的代码全部拷贝至SDRAM内存中,之后跳转到SDRAM中继续执行。当跳转到内存中开始执行后,这4KB的SRAM就可以随便使用了。
        需要注意的是,在以Nand方式启动时,硬件在自动读取Nand Flash前4KB的内容时,并没有使用ECC校验,所以必须确保Nand Flash上前4KB的代码没有位错误。
        这个流程看似很完美,但我有一点疑惑,就是此时Nand控制器还没有被初始化,cpu是怎么能够访问NandFlash来读取其上存储的数据的呢?查看S3C2440的用户手册,它说当硬件复位过程中,Nand Flash控制器会通过如下几个SoC的引脚来获取外接的Nand Flash芯片的信息:

  • NCON(Adv flash)
  • GPG13(Page size)
  • GPG14(Address cycle)
  • GPG15(Bus width)
    【韦东山旧1期学习笔记】08.S3C2440 代码重定位实验
            这里我们初步研究了一下为什么在最开始还没有初始化Nand Flash控制器时,硬件就可以正常访问Nand Flash的内容,而系统启动之后,必须要初始化Nand Flash控制器后,才能正常读取Nand Flash的内容。我把深入探究放在后续的Nand实验的文章中来探讨,这里就先这样。
            从Nand方式的启动流程中我们可以看出,如果待烧写的BIN文件的大小小于4KB,那么我们就可以不用考虑把代码拷贝到SDRAM中的事情。但是如果BIN文件的大小超过了4KB,那么我们就必须将代码拷贝到SDRAM中。既然是要将代码拷贝到新的地址空间中,那么显然这就涉及到了代码的重定位问题。

s3c2440的NorFlash启动流程

        当系统以Nor Flash方式启动时,由于Nor Flash可以像内存一样直接读取其中的数据,所以表面上看起来我们不需要像Nand方式启动那样,需要考虑4KB的问题。但是实际不是这样的。如果我们在代码中使用了全局变量,显然此时全局变量的地址也在Nor Flash对应的地址空间内。而Nor Flash是不能像内存一样直接去进行写操作的,而必须按照特定的时序来进行写入,所以对于代码中的全局变量修改操作,就会修改失败。而通过上面的介绍我们可知,这些全局变量位于.data段或者.bss段中,我们就需要将这些段重定位到SDRAM中,否则代码运行就不正常。
        下面我们通过实验来证明:当以Nor Flash方式启动时,如果代码中存在对全局变量的修改,同时我们没有进行重定位操作,那么程序就会工作不正常。