一. 前言
相信大家在刚刚学习C语言函数的时候,老师会跟我们讲,C语言函数在运行时,系统首先会给其创建一个栈,函数中的代码都是在栈中运行,当函数运行完,栈也会被销毁。以及C语言函数中的变量都是临时变量,函数退出后,这些变量不能再被访问等等。本文通过将C语言函数的汇编代码进行分析,学习C语言的本质。本文采用的开发板是S3C2440,参考的韦东山老师的案例。
二. 代码实现
1. 汇编代码 - start.S
.text
.global _start
_start:
ldr sp, =4096
bl main
halt:
b halt
.text :代码首先通过.text定义了代码段。
_start :_start是汇编语言的入口,必须要有。
ldr sp, =4096 :将栈设置在4096的位置。
bl main :跳转到main函数运行,并将lr设置位置下一跳指令的地址。
2. C语言代码 - main.c
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
*pGPFCON = 0x100;
*pGPFDAT = 0;
return 0;
}
pGPFCON :指向内存0x56000050的位置。
pGPFDAT :指向内存0x56000054的位置。
*pGPFCON = 0x100 :在内存0x56000050位置写入0x100。
*pGPFDAT = 0 :在内存0x56000054位置写入0。
3. 代码编译
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm *.bin *.o *.elf
led.c和start.S分别编译为led.o和start.o,led.o和start.o链接生成led.elf,led.elf通过objdump处理生成led.bin,led.bin可以烧写到板子上运行。通过objump反编译led.elf生成led.dis,接下来分析led.dis文件。
三. 反汇编代码分析
反汇编代码led.dis文件内容如下,接下来通过图示一行行分析代码,体会C语言函数栈。
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: eb000000 bl c <main>
00000008 <halt>:
8: eafffffe b 8 <halt>
0000000c <main>:
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: 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.
反汇编代码的第一列是代码在内存的运行的位置,例如0;第二列是机器码,例如e3a0da01;第三列是汇编指令,例如mov sp, #4096;第四列是注释,例如"; 0x1000"。
0: e3a0da01 mov sp, #4096 ; 0x1000
将栈指针指针内存4096的位置。
4: eb000000 bl c <main>
跳转到main函数运行,并将lr设置为下一条指令的地址8。lr = 8
c: e1a0c00d mov ip, sp
ip = sp = 4096
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
stmdb是按寄存器的序号从高地址到低地址压入sp指向的内存的,!表示sp最终的值会改变。如下图所示。其中pc等于当前地址加8,所以pc = 18;lr = 8已知;ip = 4096已知;fp未知,sp会递减四次,sp = 4080。
14: e24cb004 sub fp, ip, #4 ; 0x4
fp = ip - 4 = 4096 - 4 = 4092
18: e24dd008 sub sp, sp, #8 ; 0x8
sp = sp - 8 = 4080 - 8 = 4072
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
r3 = 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
r3 = r3 + 80 = 0x56000000 + 0x40 = 0x56000050
24: e50b3010 str r3, [fp, #-16]
fp - 16 = 4092 - 16 = 4076,也就是将r3的值存入到内存4076。
28: e3a03456 mov r3, #1442840576 ; 0x56000000
r3 = 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
r3 = 0x56000054
30: e50b3014 str r3, [fp, #-20]
r3 => [ 4092 - 20 ] = [ 4072 ]
34: e51b2010 ldr r2, [fp, #-16]
[ 4092 - 16 ] = [ 4076 ] => r2 = 0x56000050
38: e3a03c01 mov r3, #256 ; 0x100
r3 = 0x100
3c: e5823000 str r3, [r2]
r3 = 0x100 => [r2] = [ 0x56000050 ],对应C语言代码*pGPFCON = 0x100;
40: e51b2014 ldr r2, [fp, #-20]
[ fp - 20 ] = [ 4092 - 20 ] = [ 4072 ] => r2,r2 = 0x56000054
44: e3a03000 mov r3, #0 ; 0x0
r3 = 0
48: e5823000 str r3, [r2]
r3 = 0 => [ 0x56000054 ]
4c: e3a03000 mov r3, #0 ; 0x0
r3 = 0
50: e1a00003 mov r0, r3
r0 = r3 = 0
54: e24bd00c sub sp, fp, #12 ; 0xc
sp = fp - 12 = 4092 - 12 = 4080
58: e89da800 ldmia sp, {fp, sp, pc}
程序的pc指针为8,所以下一条执行的指令是 b 8 <halt>
四. 总结
通过指定sp寄存器的值,为C语言函数准备栈,然后通过stmdb初始化栈,然后运行函数的处理逻辑,最后调用ldmia恢复栈,栈中的数据都是临时数据,恢复栈时,栈中的数据并不会保存下来。通过这个示例,可以更加深入的了解C语言的函数的本质。