C程序的内部机制
1. 概述
通过分析C语言点亮LED的程序来了解C程序的内部机制
C程序:在main函数中,主要做了如下的事情:
- 1. 定义两个局部变量;
- 2.给这两个局部变量设置值;
- 3.返回0值
int main(void *argc, void **argv)
{
unsigned int *pGPFCON = (unsigned int*)0x56000050;
unsigned int *pGPFDAT = (unsigned int*)0x56000054;
//将GPF4功能配置为输出(GPF4[9:8] = 0x01)
*pGPFCON = 0x100;
//设置GPFDAT的bit设置为0,点亮LED
*pGPFDAT = 0x00;
return 0;
}
汇编代码:在start.S文件中的汇编程序,主要做了如下的事情:
- 1. 设置栈在内存中的地址;
- 2. 跳转到main函数,并将从main返回的值保存到lr中;
- 3:然后死循环halt
.text
.global _start
_start:
/*设置栈的地址*/
ldr sp, =4096 //nand Flash方式启动时,SRAM的最高地址设置为栈首地址
//ldr sp, = 0x40000000 + 4096 //nor Flash方式启动时,SRAM的基地址是0x40000000
/*跳转到main执行main程序*/
bl main
halt:
b halt
反汇编C程序:
- 对于以Nand Flash方式启动,S3C2440A的硬件会将Nand Flash中的前4K拷贝到4K SRAM中
- 以下的反汇编代码会完完全全的赋值到4K SRAM中。
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
//[0]: CPU读取0地址,第一条指令就是设置栈的地址,栈指向的是4K SRAM的最高地址
0: e3a0da01 mov sp, #4096 ; 0x1000
//[1]: 第二条指令,就是跳转到C程序的main函数,main函数的地址在0x0000000c
4: eb000000 bl c <main>
00000008 <halt>:
8: eafffffe b 8 <halt>
0000000c <main>:
//[2]: 跳转到main函数中,第一条指令就是将栈地址保存到CPU的ip寄存器中
c: e1a0c00d mov ip, sp
//[3]: main函数中,第二条指令就是按内存高地址到内存低地址异常将pc、lr、ip、fp寄存器中的值保存到栈内存中。
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
//[4]: 执行 fp = ip - 4
14: e24cb004 sub fp, ip, #4 ; 0x4
//[5]: 执行 sp = sp - 16
18: e24dd010 sub sp, sp, #16 ; 0x10
1c: e50b0010 str r0, [fp, #-16]
20: e50b1014 str r1, [fp, #-20]
24: e3a03456 mov r3, #1442840576 ; 0x56000000
28: e2833050 add r3, r3, #80 ; 0x50
2c: e50b3018 str r3, [fp, #-24]
30: e3a03456 mov r3, #1442840576 ; 0x56000000
34: e2833054 add r3, r3, #84 ; 0x54
38: e50b301c str r3, [fp, #-28]
3c: e51b2018 ldr r2, [fp, #-24]
40: e3a03c01 mov r3, #256 ; 0x100
44: e5823000 str r3, [r2]
48: e51b201c ldr r2, [fp, #-28]
4c: e3a03000 mov r3, #0 ; 0x0
50: e5823000 str r3, [r2]
54: e3a03000 mov r3, #0 ; 0x0
58: e1a00003 mov r0, r3
5c: e24bd00c sub sp, fp, #12 ; 0xc
60: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
其中.comment表示的注释,在bin文件中是没有这些注释内容的。
2. 几个问题
2.1 为什么要设置栈?
2.2 怎么使用栈?
- 1. 使用栈来保存局部变量
- 2. 使用栈保存lr等寄存器
- lr用来保存main函数的返回值。
2.3 调用者如何给被调用者传参?
根据ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准),调用者通过r0、r1、r2、r3将参数传递给被调用者。
2.4 被调用者如何给调用者传返回值?
根据ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准),被调用者也是通过r0、r1、r2、r3寄存器将结果返回给调用者。
2.5 如何恢复保存在栈中的寄存器?
在程序中CPU的r4~r11寄存器可能会被使用。因此需要在函数的入口需要保存这些寄存器。在函数的出口,需要恢复它们。如果程序比较简单,程序用到哪个寄存器就保存哪个寄存器。
3. 汇编程序执行分析
以下是反汇编程序指令存放在4K SRAM中的图示:
3.1 程序指令执行的过程
- 1. 板子上电,CPU从0地址读取第一条指令0xe3a0da01,即mov sp, #4096,它将栈地址设置为SRAM的最高地址4096, sp = 4096
- 2. 然后,CPU读取第二条指令0xeb000000,跳转到main函数(地址0xc)执行,同时将下一条指令的地址0x8保存到lr寄存器中。
- 3. CPU读取保存在0xc内存地址的指令0xe1a0c00d执行,即mov ip, sp,它将栈地址保存到ip寄存器中。
- 4. CPU读取保存在0x10内存地址中的指令0xe92dd800执行,即stmdb sp!, {fp, ip, lr, pc},它会将pc、lr、ip、fp寄存器中的值依次从栈的高地址到地址保存到栈中。都保存到栈中后,此时sp指向内存地址4076
- pc中保存的是当指令地址 + 8的地址,当前指令0xe92dd800的地址是0x10,所以pc = 0x18
- lr = 0x8
- ip = 4096
- fp中的值未知
此时栈中,保存的值是
- 5. CPU读取0xe24cb004指令,即sub fp, ip, #4 ----> fp = ip - 4 = 4096 - 4 = 4092
- 6. CPU读取0xe24dd010指令,即sub sp, sp, #16 ----> sp = sp -16 = 4080 - 16 = 4064
- 7. CPU读取0xe50b0010指令,即str r0, [fp, #-16],----> [fp - 16] = [4092 - 16] = [4076] = r0,即将在r0寄存器中的数据保存到地址4080中,r0保存的是main函数的第一个形参argc局部变量的值,即r0 = argc,此时栈:
- 8. CPU读取0xe50b1014指令,即str r1, [fp, #-20] ----> [ fp, #-20] = [4092 - 20] = [4072] = r1,将r1寄存器中的值保存到内存地址4072中,r1中保存的是main中第二个形参argv局部变量的值,此时的栈:
- 9. CPU读取0xe3a03456指令,即mov r3, #1442840576 ; 0x56000000 —> r3 = 0x56000000
- 10. CPU读取0xe2833050指令,即add r3, r3, #80 ; 0x50 —> r3 = r3 + 0x50 = 0x56000050
- 11. CPU读取0xe50b3018指令,即str r3, [fp, #-24] —> [fp - 24] = [4092 - 24] = [4068] = r3,即将寄存器r3中的值保存到内存地址4068中,r3中的值是0x56000050,此时栈:
- 12. CPU读取0xe3a03456指令,即mov r3, #1442840576 ; 0x56000000 —> r3 = 0x56000000
- 13. CPU读取0xe2833054指令,即add r3, r3, #84 ; 0x54 —> r3 = r3 + 0x54 = 0x56000054
- 14. CPU读取0xe50b301c指令,即str r3, [fp, #-28] —> [fp - 28]= [4092 - 28] = [4064] = r3,即将寄存器r3中的值保存到内存地址4064中,r3中的值是0x56000054,此时栈:
- 15. CPU读取0xe51b2018指令,即ldr r2, [fp, #-24] ----> r2 = [fp -24] = [4092 - 24] = [4068],即将存储在内存地址4068的值保存到r2寄存器中,即r2 = 0x56000050
- 16. CPU读取0xe3a03c01指令,即mov r3, #256 ; 0x100 —> r3 = 0x100,将立即数0x100保存到r3寄存器中
- 17. CPU读取0xe5823000指令,即str r3, [r2],将保存在r3中的值0x100,保存到内存地址0x56000050中
- 18. CPU读取0xe51b201c指令,即ldr r2, [fp, #-28] ----> r2 = [fp - 28] = [4092 - 28] = [4064],即将存储在内存地址4064的值保存到r2寄存器中,即r2 = 0x56000054
- 19. CPU读取0xe3a03000指令,即mov r3, #0 ; 0x0 ----> r3 = 0x0,将0保存到r3寄存器中
- 20. CPU读取0xe5823000指令,即str r3, [r2],即将保存在r3寄存器中的值0存储到内存地址0x56000054中
- 21. CPU读取0xe3a03000指令,即mov r3, #0 ; 0x0,将0保存到r3寄存器中,这里0其实是return 0的0值
- 22. CPU读取0xe1a00003指令,即mov r0, r3 ----> 将r3中的0值保存到r0中,即r0中保存的值就是main程序的返回值0。
- 23. CPU读取0xe24bd00c指令,即sub sp, fp, #12 ; 0xc ----> sp = fp -12 = 4092 - 12 = 4080,恢复栈指针sp在进入main程序之前的最后sp指向的地址,开始准备恢复进入main之前的环境
- 24. CPU读取0xe89da800指令,即ldmia sp, {fp, sp, pc},开始恢复调用main之前的环境:
- 1. 首先,将内存地址4080中的值,加载到fp寄存器中,即fp = 未知数
- 2. 然后, sp’ = 4080 + 4 = 4084,实际的sp仍然是sp = 4080
- 3. 再然后,将4084中的值,加载到sp栈寄存器中,即sp此时指向4096
- 4. 接着,sp’ = 4084 + 4 = 4088,实际的sp 仍然是sp = 4096
- 5. 然后, 将地址4088中的值加载到pc寄存器中,即pc = 0x8
- 6. 最后,跳转到halt标签,死循环。