首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

ARM裸机程序研究 - 编译和链接(2)

ARM裸机程序研究 - 编译和链接(2)

start address就是开始执行的入口点, 这个地址对应反汇编中的"_start"符号。

    那么可以让程序不链接到c运行库么?当然可以,可以用ld手工链接:

    ld test.o -e main -o test

   “-e main”告诉ld链接器用main函数作为入口点。这里也可以看出,一个程序的入口函数,不一定是main,可以是任意函数。再次反汇编刚生成的可执行文件,就会发现,已经没有c运行库的代码了。

   可是,如果试着执行刚刚生成的程序,竟然会得到一个段错误……这是因为,没有了c运行库,main函数返回之后,程序执行到不确定的地方。而如果通过c运行库调用main(),返回后会到c运行库里面,会调用相关函数来结束进程。


2. 裸机程序的实现
    所谓裸机程序,也就是没有操作系统支持,芯片上电后就可以开始执行的程序,就和单片机程序一样。不知道用”裸机程序“这个名称是否合适,不过也找不到其他的名字了。
    裸机程序与上面的ELF可执行文件有什么不同,首先很明显一点,ELF文件是需要有一个解析器,或者叫装载器的, 这个装载器负责解析文件头,将其中的节都映射到进程空间,如果有重定位,要先完成重定位,如果有动态链接库,还要加载动态链接库,完成种种初始化之后,才跳转到程序的入口点开始执行程序。而所有这些,都是由OS支持的。而对于一个ARM芯片来说,他可不知道什么ELF,重定位和动态链接。ARM只知道上电后,寄存器复位到初始值,PC寄存器为0x00000000,也就是从内存地址为0的地方开始取指令执行,其它的一概不知道,也不管。
    这么说来,要弄出一个裸机程序,其实也不难,只要我们编译上面的源代码,然后想办法把它加载到内存0开始的地方就可以了。事实,也确实是这样。只是有几个小问题要先解决掉:
    1.从0开始的内存从哪来?那个地方为什么会有内存?
    2.如何把程序放到内存0开始的地方
    3.就算是一个简单的main()函数,也需要栈。谁来负责设置栈?
    首先看1,一般ARM芯片都会外接一定数量的ROM和RAM。而从0开始的地址一般都会映射到ROM上,这样上电后,CPU才能取到指令执行。不过这样给调试程序带来了一点困难,ROM里面的代码不容易修改。如果想反复修改程序,调试程序,就不太方便。当然,ARM CPU都还有外接的RAM,不过这些大都是SDRAM。 SDRAM在芯片初始化的时候是还不能用的,需要初始化SDRAM控制器,设置一些初始值才行。
    我现在有的开发板是QQ2440,使用的samsung S3C2440的SOC。2440有一个很好的特性,就是可以从NAND启动。CPU是不能直接访问NAND存储器的,需要通过NAND控制器。也就是说,不能把NAND里面的内容直接映射到CPU的地址空间。为此,2440里面有一个叫“steppingsone”的地方,其实就是一块4K 的RAM。当设置从NAND启动时,上电后,2440里面的复位逻辑会先从NAND里面把前4K的内容读出来,放到这个steppingstone里面,因为这个RAM是映射到地址0开始的,当CPU开始执行程序的时候,就能够顺利的取到指令。一般这里面的程序会初始化SDRAM,把剩余的程序都复制到RAM里面,然后跳转的RAM开始执行。不过对于我们的试验来说,刚开始完全可以在这个4K的steppingstone里面来完成。
    第二个问题,最直接的办法,就是把程序烧在ROM或NAND里面,映射到地址为0的地方。不过对于试验来说,有些不太方便。第二种方法是通过JTAG接口下载,我就是用的这种方法,使用QQ2440自带的并口小板和openocd,这种方法灵活性最大。还有一种方法,一般开发板自带的ROM里面都会有预装的bootloader。它可以通过串口或者USB从PC上下载程序到内存指定的地方,然后跳转过去执行。这种方法也很方便。
    第三个问题,因为c程序的最小单位就是函数,函数执行是需要栈的,用来存储一些局部变量和保存返回地址。其实初始化栈只要将栈基址寄存器设置在内存中的合适的地方就可以了,只是这点小动作需要用一点点汇编语言来完成。
    用编辑器创建下面的汇编源文件文件:
[plain] view plaincopy

  • .section .init  
  • .global _init  

  • _init:  
  •         ldr sp, =0x00001000  
  •         bl mymain  

  • loop:   b loop  

    这段代码里面,定义一个名为“.init"得节, 然后实际的指令就两个,将0x00001000装入sp寄存器,和跳转到mymain执行。sp是栈指针,0x00001000刚好是4K,也就是我们将栈设置在了4k的地方,也就是steppingstone的最末尾,因为栈是从内存高端向低端增长的。 后面的“b loop"是一个死循环,这样mymain返回的话,就会停在这里,不至于执行到不确定的地方。
    把这个源文件保存为init.S,使用ARM交叉编译器编译:
    arm-linux-as init.S -o init.o
    生成的init.o文件,也可以用arm-linux-objdump 看一下,是不是期望的内容。我们所期望得,就是里面应该有一个.init节,该节的反汇编代码,也就是源代码里的3条指令。
    有了这段小汇编代码来设置最基本的C运行环境,下面就可以用C语言来编程了。首先是一段最简单的,就是点亮qq2440开发板上的4个LED。
[plain] view plaincopy

  • #define GPBCON  (*(unsigned long*)0x56000010)  
  • #define GPBDAT  (*(unsigned long*)0x56000014)  
  • #define GPBUP   (*(unsigned long*)0x56000018)  
  • #define WTCON   (*(unsigned long*)0x53000000)  
  • int mymain()  
  • {  
  •     WTCON = 0; /* turn off watch dog. */  

  •     unsigned long v = GPBCON;  
  •     v &= 0xFFFc03FF;  
  •     v |= 0x00015400;  
  •     GPBCON = v;  

  •     v = GPBDAT;  
  •     v &= ~0x000001e0;  
  •     GPBDAT = v;    /* turn on all LEDs */  

  •     return 0;  
  • }  



    关于2440的GPIO控制,可以查看其数据手册。这段代码用宏定义了些寄存器的地址,这些地址都可以参考数据手册。接下来,是mymain函数。首先通过设置WTCON寄存器来关闭看门狗。2440中看门狗在复位后默认是开启状态,如果不关闭,芯片在其超时后会自动复位。然后,通过设置GPBCON和GPBDAT寄存器来点亮LED。

    将上面的c源文件保存为led.c, 用gcc编译
    gcc -c led.c -o led.o
    这样就会得到一个包含编译后可执行代码的led.o文件,其中的.text节包含的就是二进制代码,可以使用arm-linux-objdump查看。现在的情况是:我们有个init.o文件,其中.init节保存有需要最开始执行的初始代码。还有一个led.o文件,其中.text节保存的是c源文件编译后的可执行二进制代码。而我们需要的,是将init.o中的.init节和led.o中的.text节拿出来拼接在一起,并且保证.init节的代码放在最开始。这就需要链接器了。但是默认情况下链接器完成不了这个工作,前面说过,默认情况下,链接器会链接c运行库,而且会寻找main函数入口点。更甚,在现在的这种情况下,链接器跟本不知道需要链接哪些节,以及如何安排这些节的位置。我们需要通过额外的办法来指导链接器完成我们需要的工作,这个就是链接脚本。链接脚本的文档可以在gnu.org上找到。这里,我们只需要一个非常简单的脚本,如下:
[plain] view plaincopy

  • SECTIONS  
  • {  
  •     .text : {*(.init) *(.text)}  
  • }  
继承事业,薪火相传
返回列表