1.4 映像文件的生成和运行

德国罕见的科学大师莱布尼茨,在他的手迹里留下这么一句话:“1与0,一切数字的神奇渊源。这是造物的秘密美妙的典范,因为,一切无非都来自上帝。”二进制0和1两个简单的数字,构造了神奇的计算机世界,对人类的生产活动和社会活动产生了极其重要的影响,并以强大的生命力飞速发展。在嵌入式系统移植过程中,不管文件数量多么庞大,经过编译工具的层层处理后,最终生成一个可以加载到存储器内执行的二进制映像文件(.bin)。本节内容将会探讨映像文件的生成过程,以及它在存储设备的不同位置对程序运行产生的影响,为本书后文嵌入式系统的移植打下坚定的基础。

1.4.1 编译过程

GNU提供的编译工具包括汇编器as、C编译器gcc、C++编译器g++、链接器ld、二进制转换工具objcopy和反汇编的工具objdump等。它们被称作GNU编译器集合,支持多种计算机体系类型。基于ARM平台的工具分别为arm-linux-gcc、arm-linux-g++、arm-linux-ld、arm-linux-objcopy和arm-linux-objdump。arm-linux交叉编译工具链的制作方法已经详细介绍过了,编译程序直接使用前面制作好的工具链。

GNU 编译器的功能非常强大,程序可以用 C 文件、汇编文件编写,甚至是二者的混合。如图 1.3 所示是程序编译的大体流程,源文件经过预处理器、汇编器、编译器、链接器处理后生成可执行文件,再由二进制转换工具转换为可用于烧写到 Flash 的二进制文件,同时为了调试的方便还可以用反汇编工具生成反汇编文件。图中双向箭头的含义是,当gcc增加一些参数时可以相互调用汇编器和链接器进行工作。例如输入命令行“gcc -O main.c”后,直接就得到可执行文件a.out(elf)。

图1.3 程序编译流程

程序编译大体上可以分为编译和链接两个步骤:把源文件处理成中间目标文件.o(linux)、obj (windows)的动作称为编译;把编译形成的中间目标文件以及它们所需要的库函数.a(linux)、lib (windows)链接在一起的动作称为链接。现用一个简单的test工程来分析程序的编译流程。麻雀虽小,五脏俱全,它由启动程序start.S、应用程序main.c、链接脚本test.lds和Makefile四个文件构成。test工程中的程序通过操作单板上的LED灯的状态来判定程序的运行结果,它除了用于理论研究之外,没有其他的实用价值。

1.编译

在编译阶段,编译器会检查程序的语法、函数与变量的声明情况等。如果检查到程序的语法有错误,编译器立即停止编译,并给出错误提示。如果程序调用的函数、变量没有声明原型,编译器只会抛出一个警告,继续编译生成中间目标文件,待到链接阶段进一步确定调用的变量、函数是否存在。

start.S文件的内容如程序清单1.1所示,文件中的_start函数,为程序能够在C语言环境下运行做了最低限度的初始化:将S3C6410处理外设端口的地址范围告知ARM内核,关闭看门狗,清除bss段,初始化栈。初始化工作完毕后,跳转到main()。start.S是用汇编语言编写的代码文件,文件中定义了一个WATCHDOG宏,用于寄存器的赋值。在汇编文件中出现#define宏定义语句,对于初学者可能会有些迷惑。

程序清单1.1 start.S中汇编代码

        /*
          *     This is a part of the test project
          *     Author: LiQiang Date: 2013/04/01
          *     Licensed under the GPL-2 or later.
          */
        .globl _start
        _start:
          #define REG32 0x70000000
          ldr r0, =REG32
          orr r0, r0, #0x13
          mcr p15,0,r0,c15,c2,4
          /*关闭看门狗*/
          #define WATCHDOG 0x7E004000
          ldr r0, =WATCHDOG
          mov r1, #0
          str r1, [r0]
        clean_bss:
            ldr r0, =bss_start
            ldr r1, =bss_end
            mov r3, #0
            cmp r0, r1
            beq clean_done
        clean_loop:
            str r3, [r0], #4
            cmp r0, r1
            bne clean_loop
        clean_done:
          /* 初始化栈 S3C6410 8K的SRAM映射到0地址处*/
          ldr sp, =8*1024
          bl main
        halt:
            b halt

事实上,汇编文件有“.S”和“.s”两种后缀,在以“.s”为后缀的汇编文件中,程序完全是由纯粹的汇编代码编写的。所谓的纯粹是相对以“.S”为后缀的汇编文件而言的,由于现代汇编工具引入了预处理的概念,允许在汇编代码(.S)中使用预处理命令。预处理命令以符号“#”开头,包括宏定义、文件包含和条件编译。在U-Boot和Linux内核源码中,这种编程方式运用非常广泛。

main.c文件内容如程序清单1.2所示,main.c中的main函数是运行完_start函数的跳转点。main()中首先定义了一个静态局部变量,初值为12,然后配置S3C6410处理器的GPM端口为输出、下拉模式,并将GPM端口低四位对应的管脚设为高电平(LED驱动管脚的电平为高时,LED熄灭)。最后判断flag是否等于12,如果等于就点亮LED,否则不点亮。从程序上看,这个判断语句好像多此一举、莫名其妙,因为flag期间并没有做任何改变。其实,这个变量是为讲解程序的运行地址和加载地址的概念而定义的,它与程序运行的位置有关。

程序清单1.2 main.c文件内容

        /*
        *  This is a part of the test project
        *   Author: LiQiang Date: 2013/04/01
        *   Licensed under the GPL-2 or later.
        */
        #define GPMCON  *((volatile unsigned long*)0x7F008820)
        #define GPMDAT  *((volatile unsigned long*)0x7F008824)
        #define GPMPUD  *((volatile unsigned long*)0x7F008828)
        int main()
        {
            static int flag = 12;
            GPMCON = 0x1111; /* 输出模式 */
            GPMPUD = 0x55;  /* 使能下拉 */
            GPMDAT = 0x0f;  /* 关闭LED */
            if(12 == flag)
                  GPMDAT = 0x00;
            else
                  GPMDAT = 0x0f;
            while(1);
            return 0;
        }

将上面两个源码文件处理成中间目标文件,分别输入如下命令行:

        arm-linux-gcc -o mian.o main.c -c
        arm-linux-gcc -o start.o start.S -c

得到main.o、Start.o两个中间目标文件,供链接器使用。

2.链接

链接是汇编阶段生成的中间目标文件,相互查找自己所需要的函数与变量,重定向数据,完成符号解析的过程。包括对所有目标文件进行重定位、建立符号引用规则,同时为变量、函数等分配运行地址。函数与变量可能来源于其他中间文件或者库文件,如果没有找到所需的实现,链接器立即停止链接,给处错误提示。

利用一个链接脚本(.lds后缀)来指导链接器工作。控制输出节(Outpat section)在映像文件中的布局。fortest.lds 是一个简单的链接脚本,指示了程序的运行地址(又称链接地址)为0x5000_0000以及text段、data段和bss段在映像文件中的空间排布顺序。fortest.lds文件的内容如下:

        ENTRY(_start)
        SECTIONS
        {
                . = 0x50000000;
                . = ALIGN(4);
                .text : {
                        start.o (.text)
                        * (.text)
                }
                .data : {
                        * (.data)
                }
                bss_start = .;
                .bss : {
                        * (.bss)
                }
                bss_end  = .;
        }

(1)text段代码段(text segment),通常是用来存放程序执行代码的内存区域。这块区域的大小在程序编译时就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。

(2)data段数据段(data segment),数据段是存放已经初始化不为0的静态变量的内存区域,静态变量包括全局变量和局部变量,它们与程序有着相同的生存期。

(3)bss段bss segment,bbs段与data段类似,也是存放的静态变量的内存区域。与data段不同的是,bbs段存放的是没有初始化或者初始化为0 的静态变量,并且bbs段不在生成的可执行二进制文件内。bss_start表示这块内存区域的起始地址,bss_end表示结束地址,它们由编译系统计算得到。未初始化的静态变量默认为 0,因此程序开始执行的时候,在 bss_start 到 bss_end内存中存储的数据都必须是0。

(4)其他段,上面3个段是编译系统预定义的段名,用户还能通过.section伪操作自定义段,在后面的移植过程中会发现,Linux 内核源码中为了合理地排布数据实现特定的功能,定义了各种各样的段。

在宿主机上输入以下命令行,完成中间的目标文件的链接和可执行二进制文件的格式转换。

        arm-linux-ld -T test.lds -o test.elf start.o main.o
        arm-linux-objcopy -O binary test.elf test.bin
        arm-linux-objdump -D test.elf > test.dis

如图1.4所示是使用arm-linux-objcopy格式转换工具得到的二进制文件test.bin的内容,这些内容是处理器能够识别的机器码,我们往往难以直接阅读、理解它们的含义。使用arm-linux-objdump工具生成便于我们阅读的反汇编文件test.dis。

图1.4 二进制镜像文件内容

对比二进制文件test.bin的内容,耐心细致地分析反汇编文件,如程序清单1.3所示,可以提炼出大量的信息。

程序清单1.3 text.dis文件内容

        50000000 <_start>:  /* 代码段起始位置程序的运行地址为0x5000_0000*/
        50000000:    e3a00207     mov r0, #1879048192   ; 0x70000000
        50000004:    e3800013     orr r0, r0, #19  ; 0x13
        50000008:    ee0f0f92     mcr 15, 0, r0, cr15, cr2, {4}
        5000000c:    e59f0030     ldr r0, [pc, #48]; 50000044 <halt+0x4>
        50000010:    e3a01000     mov r1, #0   ; 0x0
        50000014:    e5801000     str r1, [r0]
        50000018 <clean_bss>: /* 清除bss段 */
        50000018:    e59f0028     ldr r0, [pc, #40]; 50000048 <halt+0x8>
        5000001c:    e59f1028     ldr r1, [pc, #40]; 5000004c <halt+0xc>
        50000020:    e3a03000     mov r3, #0   ; 0x0
        50000024:    e1500001     cmp r0, r1
        50000028:    0a000002     beq 50000038 <clean_done>
        5000002c <clean_loop>:
        5000002c:    e4803004     str r3, [r0], #4
        50000030:    e1500001     cmp r0, r1
        50000034:    1afffffc     bne 5000002c <clean_loop>
        50000038 <clean_done>:
        50000038:    e3a0da02     mov sp, #8192    ; 0x2000 /* 初始化sp */
        5000003c:    eb000003     bl  50000050 <main> /* 跳转至mian() */
        50000040 <halt>:
        50000040:    eafffffe     b    50000040 <halt>
        50000044:    7e004000     .word    0x7e004000
        50000048:    500000e0     .word    0x500000e0
        5000004c:    500000e0     .word    0x500000e0
        50000050 <main>: /* main()*/
        50000050:    e52db004     push{fp}     ; (str fp, [sp, #-4]!)
        50000054:    e28db000     add fp, sp, #0   ; 0x0
        50000058:    e3a0247f     mov r2, #2130706432   ; 0x7f000000
        5000005c:    e2822b22     add r2, r2, #34816    ; 0x8800
        50000060:    e2822020     add r2, r2, #32  ; 0x20
        50000064:    e3a03c11     mov r3, #4352    ; 0x1100
        50000068:    e2833011     add r3, r3, #17  ; 0x11
        5000006c:    e5823000     str r3, [r2]  /*  GPMCON = 0x1111 */
        50000070:    e3a0347f     mov r3, #2130706432   ; 0x7f000000
        50000074:    e2833b22     add r3, r3, #34816    ; 0x8800
        50000078:    e2833028     add r3, r3, #40  ; 0x28
        5000007c:    e3a02055     mov r2, #85  ; 0x55
        50000080:    e5832000     str r2, [r3]  /*  GPMPUD = 0x55 */
        50000084:    e3a0347f     mov r3, #2130706432   ; 0x7f000000
        50000088:    e2833b22     add r3, r3, #34816    ; 0x8800
        5000008c:    e2833024     add r3, r3, #36  ; 0x24
        50000090:    e3a0200f     mov r2, #15  ; 0xf
        50000094:    e5832000     str r2, [r3]  /* GPMDAT = 0x0f */
        50000098:    e59f3038     ldr r3, [pc, #56]; 500000d8 <main+0x88>  /* 读取flag变量存储地址 */
        5000009c:    e5933000     ldr r3, [r3]/* 读取flag变量的值 */
        500000a0:    e353000c     cmp r3, #12  ; 0xc
        500000a4:    1a000005     bne 500000c0 <main+0x70>
        500000a8:    e3a0347f     mov r3, #2130706432   ; 0x7f000000
        500000ac:    e2833b22     add r3, r3, #34816    ; 0x8800
        500000b0:    e2833024     add r3, r3, #36  ; 0x24
        500000b4:    e3a02000     mov r2, #0   ; 0x0
        500000b8:    e5832000     str r2, [r3]
        500000bc:    ea000004     b    500000d4 <main+0x84>
        500000c0:    e3a0347f     mov r3, #2130706432   ; 0x7f000000
        500000c4:    e2833b22     add r3, r3, #34816    ; 0x8800
        500000c8:    e2833024     add r3, r3, #36  ; 0x24
        500000cc:    e3a0200f     mov r2, #15  ; 0xf
        500000d0:    e5832000     str r2, [r3]
        500000d4:    eafffffe     b    500000d4 <main+0x84>
        500000d8:    500000dc     .word    0x500000dc
        Disassembly of section .data:
        500000dc <flag.1245>: /* flag变量的地址为0x5000_00dc,值为12 */
        500000dc:    0000000c     .word    0x0000000c

从test.dis反汇编文件中可知,test.bin包含了代码段和数据段,并没有包含bss段。我们知道, bbs 内存区域的数据初始值全部为零,区域的起始位置和结束位置在程序编译的时候预知。很容易想到在程序开始运行时,执行一小段代码将这个区域的数据全部清零即可,没必要在 test.bin包含全为0的bss段。编译器的这种机制有效地减小了镜像文件的大小,节约了磁盘容量。

main()函数的核心功能是验证flag变量是否等于12,现在追踪下这个操作的实现过程。要想读取flag的值,必须知道它的存储位置,首先执行指令“ldrr3, [pc, #56]”得到flag变量的地址(指针)。pc与56相加合成一个地址,它是相对pc偏移56产生的。pc+56地址处存放了flag变量的指针0x5000_00dc,读取出来存放到r3寄存器。然后执行指令“ldrr3, [r3]”,将内存0x5000_00dc地址处的值读出,这个值就是 flag,并覆盖 r3 寄存器。最后,判断 r3 寄存器是否等于 12。flag变量的地址在链接阶段已经被分配好了,固定在0x5000_00dc处,但是从代码中,我们没有找到对flag变量赋初值的语句,尽管在main函数已经用C语句“flag = 12”对它赋初值。

现提供一个验证程序效果的简单方法:将 S3C6410 处理器设置为 SD 卡启动方式,使用SD_Writer软件将test.bin烧写至SD卡中,然后将SD卡插入单板的卡槽,复位启动即可。实际上,启动的时候test.bin被加载到内部SRAM中,SRAM映射到0地址处。这个简单方法可以用来验证一些裸板程序,方法实现的原理和SD_Writer软件用法现在不展开讨论,目前只要会使用即可。复位后,LED并没有点亮。

如果每次编译都要重复输入编译命令,操作起来很麻烦,为此test工程中建立了一个Makefile文件,内容如下:

        test.bin: start.o main.o
            arm-linux-ld -T fortest.lds -o test.elf start.o main.o
            arm-linux-objcopy -O binary test.elf test.bin
            arm-linux-objdump -D test.elf > test.dis
        start.o : start.S
            arm-linux-gcc -o start.o start.S -c
        main.o : main.c
            arm-linux-gcc -o main.o main.c -c
        clean:
            rm *.o test.*

当将链接脚本中的运行地址修改为0时,进入test目录,输入“make clean”命令清除旧的文件,再输入“make”重新编译程序,验证新生成的test.bin文件的效果,发现LED全部点亮,产生这个现象的原因在下一个小节讲述。