天天看点

《嵌入式Linux开发实用教程》——1.4 映像文件的生成和运行define GPMCON ((volatile unsigned long)0x7F008820)define GPMDAT ((volatile unsigned long)0x7F008824)define GPMPUD ((volatile unsigned long)0x7F008828)

本节书摘来异步社区《嵌入式linux开发实用教程》一书中的第1章,第1.4节,作者:朱兆祺 ,李强 ,袁晋蓉 ,更多章节内容可以访问云栖社区“异步社区”公众号查看

嵌入式linux开发实用教程

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

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)。

《嵌入式Linux开发实用教程》——1.4 映像文件的生成和运行define GPMCON ((volatile unsigned long)0x7F008820)define GPMDAT ((volatile unsigned long)0x7F008824)define GPMPUD ((volatile unsigned long)0x7F008828)

程序编译大体上可以分为编译和链接两个步骤:把源文件处理成中间目标文件.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.

*/

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;

}<code>`</code>

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

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内核源码中为了合理地排布数据实现特定的功能,定义了各种各样的段。

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

50000000 &lt;_start&gt;: / 代码段起始位置程序的运行地址为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

50000010:  e3a01000   mov  r1, #0  ; 0x0

50000014:  e5801000   str  r1, [r0]

50000018 : / 清除bss段 /

50000018:  e59f0028   ldr  r0, [pc, #40]  ; 50000048

5000001c:  e59f1028   ldr  r1, [pc, #40]  ; 5000004c

50000020:  e3a03000   mov  r3, #0  ; 0x0

50000024:  e1500001   cmp  r0, r1

50000028:  0a000002   beq  50000038

5000002c :

5000002c:  e4803004   str  r3, [r0], #4

50000030:  e1500001   cmp  r0, r1

50000034:  1afffffc   bne  5000002c

50000038 :

50000038:  e3a0da02   mov  sp, #8192  ; 0x2000 / 初始化sp /

5000003c:  eb000003   bl  50000050 / 跳转至mian() /

50000040 :

50000040:  eafffffe   b  50000040

50000044:  7e004000   .word  0x7e004000

50000048:  500000e0   .word  0x500000e0

5000004c:  500000e0   .word  0x500000e0

50000050 : / 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  / 读取flag变量存储地址 /

5000009c:  e5933000   ldr  r3, [r3]/ 读取flag变量的值 /

500000a0:  e353000c   cmp  r3, #12  ; 0xc

500000a4:  1a000005   bne  500000c0

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

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

500000d8:  500000dc   .word  0x500000dc

disassembly of section .data:

500000dc : / flag变量的地址为0x5000_00dc,值为12 /

500000dc:  0000000c   .word  0x0000000c<code>`</code>

从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文件,内容如下:

ldr r0, =watchdog<code>`</code>

如果使用ldr伪指令将一个函数标号读取到pc,这是一条与位置有关的跳转指令,执行的结果是跳转到函数的运行地址处。

2.运行地址与加载地址

试想一下,当系统上电复位的时候,如果test.bin刚好位于0x5000_0000地址(flag的初值12位于0x5000_00dc),pc指向0x5000_0000地址,那么这段代码按照上述flag变量的读取步骤,能够准确无误地得到结果。但是,如果test.bin位于0地址(flag的初值12位于0xdc,led不亮时的情况),pc指向0地址,程序依然从0x5000_00dc地址读取flag变量,实际上它的初值位于0xdc。这时从c语言的角度看,出现一个flag不等于它的初值的现象(期间没有改变flag)。出现错误的原因是在程序中使用了位置相关的变量,但运行地址与加载地址不一致(加载地址为0,运行地址为0x5000_0000)。由此,能够容易理解运行地址和加载地址的含义:

加载地址是系统上电启动时,程序被加载到可直接执行的存储器的地址,也就是程序在ram或者flash rom中的地址。因为有些存储介质只能用来存储数据不能执行程序,例如sd卡和nand flash等,必须把程序从这些存储介质加载到可以执行的地址处。运行地址就是程序在链接时候确定的地址,比如fortest.lds链接脚本指定了程序的运行地址为0x5000_0000,那么链接器在为变量、函数等分配地址的时候就会以0x5000_0000作为参考。当加载地址和运行地址不相等时,必须使用与位置无关码把程序代码从它的加载地址搬运至运行地址,然后使用“ldr pc, =label”指令跳转到运行地址处执行。

在嵌入式系统底层编程中,c语言和汇编两种编程语言的使用最广泛。c语言开发的程序具有可读性高,容易修改、移植和开发周期短等特点。但是,c语言在一些场合很难或无法实现特定的功能:底层程序需要直接与cpu内核打交道,一些特殊的指令在c语言中并没有对应的成分,例如关闭看门狗、中断的使能等;被系统频繁调用的代码段,对代码的执行效率要求严格的时候。事实上,cpu体系结构并不一致,没有对内部寄存器操作的通用指令。汇编语言与cpu的类型密切相关,提供的助记符指令能够方便直接地访问硬件,但要求开发人员对cpu的体系结构十分熟悉。在早期的微处理器中,由于处理器速度、存储空间等硬件条件的限制,开发人员不得不选用汇编语言开发程序。随着微处理器的发展,这些问题已经得到很好的解决。如果依然完全使用汇编语言编写程序,工作量会非常大,系统很难维护升级。大多数情况下,充分结合两种语言的特点,彼此相互调用,以约定规则传递参数,共享数据。

1.汇编函数与c语言函数相互调用

c程序函数与汇编函数相互调用时必须严格遵循atpcs(armthumb procedure call standard)。函数间约定r0、r1和r2为传入参数,函数的返回值放在r0中。gnu arm编译环境中,在汇编程序中要使用.global伪操作声明改汇编程序为全局的函数,可被外部函数调用。在c程序中要被汇编程序调用的c函数,同样需要用关键字extern声明。

程序清单1.4是从archarmcpuarm1176start.s文件(u-boot)中截取的代码片段,relocate_code函数用于重定位代码。它在c程序中,通过relocate_code(addr_sp, id, addr)被调用。变量addr_sp、id和addr分别通过寄存器r0、r1和r3传递给汇编程序,实现了c函数和汇编函数数据的共享。

程序清单1.4 代码重定位函数

armv6 up and smp safe atomic ops. we use load exclusive and

store exclusive to ensure that these are atomic. we may loop

to ensure that the update happens.

static inline void atomic_add(int i, atomic_t *v)

{  

  unsigned long tmp;  

  int result;

  __asm__ __volatile__("@ atomic_addn"

"1:  ldrex  %0, [%3]n"

"  add  %0, %0, %4n"

"  strex  %1, %0, [%3]n"

"  teq  %1, #0n"

"  bne  1b"  

  : "=&amp;r" (result), "=&amp;r" (tmp), "+qo" (v-&gt;counter)  

  : "r" (&amp;v-&gt;counter), "ir" (i)  

  : "cc");

asm语句最常用的格式为:

继续阅读