50000040
50000040: eafffffe b 50000040
50000044: 7e004000 .word 0x7e004000
50000048: 500000e0 .word 0x500000e0
5000004c: 500000e0 .word 0x500000e0
50000050
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
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
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全部点亮,产生这个现象的原因在下一个小节讲述。
1.1.1 代码搬运
当程序执行时,必须把代码搬运到链接时所指定的运行地址空间,以保证程序在执行过程中对变量、函数等符号的正确引用。在带有操作系统中,这个过程由操作系统负责完成。而在裸机环境下,镜像文件的运行地址由程序员根据具体平台指定,加载地址又与处理器的设计密切相关。通常情况下,启动代码最先执行一段位置无关码,这段代码实现程序从加载地址到运行地址的重定位,或者将程序从外部存储介质直接拷贝至其运行地址。
1. 位置无关码
位置无关码必须具有位置无关的跳转、位置无关的常量访问等特点,不能访问静态变量,都是相对pc的偏移量来函数的跳转或者常量的访问。在ARM 体系中,使用相对跳转指令b/bl实现程序跳转。指令中所跳转的目标地址用基于当前PC的偏移量来表示,与链接时分配给地址标号的绝对地址值无关,因而代码可以在任何位置正确的跳转,实现位置无关性。
使用ldr伪指令将一个常量读取到非pc的其他通用寄存器中,可实现位置无关的常量访问。例如:
ldr r0, =WATCHDOG
如果使用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”指令跳转到运行地址处执行。
1.1.2 混合编程
在嵌入式系统底层编程中,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代码重定位函数
.globl relocate_code
relocate_code:
mov r4, r0 /* save addr_sp */
mov r5, r1 /* save addr of gd */
mov r6, r2 /* save addr of destination */
/* Set up the stack */
stack_setup:
mov sp, r4
adr r0, _start
cmp r0, r6
beq clear_bss /* skip relocation */
mov r1, r6 /* r1 <- scratch for copy_loop */
ldr r3, _bss_start_ofs
add r2, r0, r3 /* r2 <- source end address */
……
程序清单1. 4是从archarmcpuarm1176start.S文件(U-Boot)中截取的代码片段,relocate_code函数用于重定位代码。它在C程序中,通过relocate_code(addr_sp, id, addr)被调用。变量addr_sp、id和addr分别通过寄存器R0、R1和R3传递给汇编程序,实现了C函数和汇编函数数据的共享。
2. C语言内嵌汇编
当需要在C语言程序中内嵌汇编代码时,可以使用gcc提供的asm语句功能。
程序清单1. 5整数原子加操作的实现
/*
* 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"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
程序清单1. 5是从Linux源码文件arch/arm/include/asm/atomic.h截取的一段代码,本节内容不分析函数的具体实现。对于初学者,这段代码看起来晦涩难懂,因为这不是标准C所定义的形式,而是gcc对C语言扩充的asm功能语句,用以在C语言程序中嵌入汇编代码。asm语句最常用的格式为:
__asm __ __volatile__(“ inst1 op1, op2, . n”
“ inst2 op1, op2, . n” /* 指令部分必选*/
...
“ instN op1, op2, . n”
: output_operands /* 输出操作数可选 */
: input_operands /* 输入操作数可选 */
: clobbered_operands /* 损坏描述部分可选*/
);
它由四个部分组成:指令部分,输出部分,输入部分,损坏描述部分。各部分使用“:”格开,指令部分必不可少,其他三部分可选,但是如果使用了后面的部分,而前面部分为空,也需要用“:”分隔,相应部分内容为空。__asm__表示汇编语句的起始,__volatile__是一个可选项,加上它可以防止编译器优化时对汇编语句删除、移动。
指令部分,指令之间使用“n”(也可以使用“;”或者“nt”)分隔。嵌入汇编指令的格式与标准汇编指令的格式大体相同,嵌入汇编指令的操作数使用占位符“%”预留位置,用以引用C语言程序中的变量。操作数占位符的数量取决于CPU中通用寄存器的总数量,占位符的格式为%0,%1,……,%n。
输出、输入部分,这两部分用于描述操作数,不同的操作数描述语句之间用逗号分隔,每个操作数描述符由限定字符串和C语言表达式组成,当限定字符串中带有“=”时表示该操作数为输出操作数。限定字符串用于指示编译器如何处理C语言表达式与指令操作数之间的关系,限定字符串中的限定字母有很多种,有些是通用的,有些跟特定的体系相关。在程序清单1. 5中:result、tmp和v->counter是输出操作数,分别赋给%0、%1和%2;v->counter和i是输入操作数,分别赋给%3和%4。其中,“r”:表示把变量放入通用寄存器中;“I”:表示0-31之间的常数。