目录
1.解析C语言的内部机制
1.把上一节编译第10节的C语言控制代码在Linux系统反汇编文件(它的功能是点亮一颗裸板的LED灯),led.dis文件传windows系统查看,然后分析这个程序时如何运行的,具体看上一节内容:点我查看
c语言代码:
int main()
{
unsigned int *pGPFCON=(unsigned int *)0x56000050;
unsigned int *pGPFDAT=(unsigned int *)0x56000054;
/*配置GPF4的引脚为输出引脚*/
*pGPFCON=0x100;
/*配置GPF4的引脚输出低电平(点亮LED)*/
*pGPFDAT=0;
return ;
}
汇编代码 .S(启动文件):
.text
.global _start
_start:
/*c语言中局部变量保存在栈中,栈对应的是一块内存*/
/*设置内存 :sp 栈*/
ldr sp ,=4096; //Nand Flash 启动
/*对于2440来说,当设置为Nand启动,从0开始的4k空间
对应的是片内内存,把栈设置在内存的顶部*/
// ldr sp ,=0x40000000+4096; //Nand Flash 启动
/*对于2440来说,当设置为Nor启动,片内4k内存的地址是0x40000000
对应的是片内内存,把栈设置在内存的顶部*/
/*调用main函数,跳转到main函数*/
bl main
halt:
b halt
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.
看起来,比直接使用汇编语言复杂很多,其实c语言最终也是要转换成汇编语言,最终转换成机器码(二进制),烧写到soc上运行的。
先捋一下思路:
1.在汇编文件中,设置栈内存的地址
2.使用bl命令调用main,且设置返回地址存入lr寄存器
3.在c程序中,有一个main函数,写寄存器的值
问题:
1.为何要设置栈?
答:因为c语言要设置局部变量,有变量就需要内存
2.如何使用栈?
答:局部变量是保存在栈中的,保存lr等寄存器
问题:
1.调用者如何向被调用者传递参数?
2.被调用者如何传返回值给调用者?
3.如何从栈中恢复寄存器?
想要了解这些问题需要对ARM-THUMB 子程序调用规则有所了解。
2.了解ARM-THUMB 子程序调用规则 ATPCS
为了使 C 语言程序和汇编程序之间能够互相调用,必须为子程序间的调用制定规则,在ARM 处理器中,这个规则被称为 ATPCS:ARM 程序和 Thumb 程序中子程序调用的规则。基本的ATPCS 规则包括寄存器使用规则、数据栈使用规则、参数传递规则。
2.1. 寄存器使用规则
ARM 处理器中有 r0~r15 共 16 个寄存器,它们的用途有一些约定的习惯,并依具这些用途定义了别名,如下表所示:
- PCS 中各寄存器的使用规则及其名称
寄存器 别名 使用规则
r15 pc 程序计数器
r14 lr 连接寄存器
r13 sp 数据栈指针
r12 ip 子程序内部调用的 scratch 寄存器
r11 v8 ARM 状态局部变量寄存器 8
r10 v7、s1 ARM 状态局部变量寄存器 7、在支持数据栈检查的 ATPCS 中为数据栈限制指针
r9 v6、sb ARM 状态局部变量寄存器 6、在支持 RWPI 的 ATPCS 中为静态基址寄存器
r8 v5 ARM 状态局部变量寄存器 5
r7 v4、wr ARM 状态局部变量寄存器 4、Thumb 状态工作寄存器
r6 v3 ARM 状态局部变量寄存器 3
r5 v2 ARM 状态局部变量寄存器 2
r4 v1 ARM 状态局部变量寄存器 1
r3 a4 参数/结果/scratch 寄存器 4
r2 a3 参数/结果/scratch 寄存器 3
r1 a2 参数/结果/scratch 寄存器 2
r0 a1 参数/结果/scratch 寄存器 1
寄存器的使用规则总结如下:
- 子程序间通过寄存器 r0~r3 来传递参数,这时可以使用它们的别名 a0~a3。被调用的子程序返回前无需恢复 r0~r3 的内容。
- 在子程序中,使用 r4~r11 来保存局部变量,这时可以使用它们的别名 v1~v8。如果在子程序中使用了它们的某些寄存器,子程序进入时要保存这些寄存器的值,在返回前恢复它们;对于子程序中没有使用到的寄存器则不必进行这些操作。在 Thumb 程序中,通常只能使用寄存器 r4~r7 来保存局部变量。
- 寄存器 r12 用作子程序间 scratch 寄存器,别名为 ip。
- 寄存器r13用作数据栈指针,别名为sp。在子程序中寄存器r13不能用作其他用途。它的值在进入、退出子程序时必须相等。
- 寄存器 r14 被称为连接寄存器,别名为 lr。它用于保存子程序的返回地址。如果在子程序中保存了返回地址(比如将 lr 值保存到数据栈中),r14 可以用作其他用途。
- 寄存器 r15 是程序计数器,别名为 pc。它不能用作其他用途。
2.2.数据栈使用规则
数据栈有两个增长方向:向内存地址减小的方向增长时,称为 DESCENDING 栈;向内地址增加的方向增长时,称为 ASCENDING 栈。
所谓数据栈的增长就是移动栈指针。当栈指针指向栈顶元素(最后一个入栈的数据)时,称为 FULL 栈;当栈指针指向栈顶元素(最后一个入栈的数据)相邻的一个空的数据单元时,称为 EMPTY 栈。
综合这两个特点,数据栈可以分为以下 4 种:
① FD Full Descending,满递减
② ED Empty Descending,空递减
③ FA Full Ascending,满递增
④ EA Empty Ascending,空递增
注意:ATPCS 规定数据栈为 FD 类型,并且对数据栈的操作是 8 字节对齐的。使用 stmdb/ldmia批量内存访问指令来操作 FD 数据栈。使用 stmdb 命令往数据栈中保存内容时,“先递减 sp 指针,再保存数据”,使用 ldmia命令从数据栈中恢复数据时, “先获得数据,再递增 sp 指针”──sp 指针总是指向栈顶元素,这刚好是 FD 栈的定义。
2.3. 参数传递规则
一般来说,当参数个数不超过 4 个时,使用 r0~r3 这 4 个寄存器来传递参数;如果参数个数超过 4 个,剩余的参数通过数据栈来传递。对于一般的返回结果,通常使用 a0~a3 来传递。
简图分析如下:
3.分析C语言的反汇编代码
3.1.知识基础
假设程序从Nand启动,对于Nand启动的程序,硬件上会把Nand Flash 前4K的内容完全复制到片内的4K内存上,那么上面的哪些机器码在程序运行就会保存在片内内存4k内存的前面。
机器码在片内RAM的存储结构简图如下所示:
3.2.从第一句开始分析代码:
开发版一上电,从0地址开始执行
接着开始执行main函数的内容了
注意:pc的值是当前的地址+8,所以pc=0x10+0x8=0x18;
再执行下面的语句:
得到的结果使用简图表示为:
接着分析下一条语句:
执行完如下图所示:
继续
因为r0-r3用来保存调用和被调用者的参数,因此,C语言中的ruturn 应该保存在这几个变量中的一个。
退出主函数时恢复栈
从栈中恢复寄存器采用的是 ldmia 指令
指令:ldm
含义:读内存读取数据,然后把读取的数据写入多个寄存器
命令解析:
例子:
ldmia sp, {fp, sp, pc}
假设:sp=4080
例子:ia的含义是过后增加(Increment After),就是先读取后增加,而且的顺序的依据是:高编号的寄存器存储在高地址
fp,sp, pc 这三个的寄存器编号分别如下所示(ARM编程手册查看)
pc->R15,sp->R13,fp->R11.所以存取的顺序是:fp-sp-pc(与指令顺序无关)
附录:
其他形式简单的描述指令的行为,意思分别是过后增加(Increment After)、预先增加(Increment Before)、过后减少(Decrement After)、预先减少(Decrement Before)。
因此执行完这条指令之后:fp = [4080-4083] 地址的内容。sp =[4084-4087]地址的内容,pc=[4088-4092] 地址的内容。
再返回去看看,我们在程序开始运行之前这些地址保存的东西是什么?
4.传递参数(调用者传参数给背调用者)
例子:
在程序中,我们并不一定需要mian函数,可以在启动文件中修改我们启动的函数
4.1编写一个 .c文件,内容如下:
void delay(volatile int s) //用于延时,需要传递一个参数
{
while(s--);
}
//volatile的作用是不需要系统优化处理,因为我们在delay中系统来看像是什么正事都没干,会把它优化掉
int led_on(int a) //用于选择点亮哪个led灯,需要一个参数
{
if(a==4)
{
/*配置GPF4的引脚为输出引脚*/
*pGPFCON=0x100;
}
else if(a==5)
{
/*配置GPF5的引脚为输出引脚*/
*pGPFCON=0x400;
}
/*配置GPF4的引脚输出低电平(点亮LED)*/
*pGPFDAT=0;
return 0;
}
4.2编写启动汇编文件(.S):
.text
.global _start
_start:
/*设置内存:sp 栈*/
ldr sp ,=4096; //Nand Flash 的初始值为4096
/*使用r0-r3 进行参数的传递*/
//把r0=4的值传递给调用者,并跳转到led_on函数
mov r0,#4
bl led_on
//把r0=1000000传递给被调用者,并跳转到延时函数
ldr r0, = 100000;
bl delay
//把r0=5传递给被调用函数
mov r0,#5
bl led_on
halt:
b halt
4.3.编写一个Makefile文件进行,对.c和.S文件进行,编译->链接->生成bin文件和反汇编文件,具体如下
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 *.dis
4.3.上传到Linux系统进行编译
4.4.编译
4.5.把bin文件传回window系统,使用oflash软件和eop烧录器进行烧录:
实验效果和我们预设的一样,LED1先亮,接着系统进入延时,延时结束LED2亮。
4.6.看看是函数是如何向被调用者传递参数的?
分析汇编代码,也就是 .dis文件的内容
.dis的文件内容如下:
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: e3a00004 mov r0, #4 ; 0x4
8: eb000012 bl 58 <led_on>
c: e59f000c ldr r0, [pc, #12] ; 20 <.text+0x20>
10: eb000003 bl 24 <delay>
14: e3a00005 mov r0, #5 ; 0x5
18: eb00000e bl 58 <led_on>
0000001c <halt>:
1c: eafffffe b 1c <halt>
20: 000186a0 andeq r8, r1, r0, lsr #13
00000024 <delay>:
24: e1a0c00d mov ip, sp
28: e92dd800 stmdb sp!, {fp, ip, lr, pc}
2c: e24cb004 sub fp, ip, #4 ; 0x4
30: e24dd004 sub sp, sp, #4 ; 0x4
34: e50b0010 str r0, [fp, #-16]
38: e51b3010 ldr r3, [fp, #-16]
3c: e2433001 sub r3, r3, #1 ; 0x1
40: e50b3010 str r3, [fp, #-16]
44: e51b3010 ldr r3, [fp, #-16]
48: e3730001 cmn r3, #1 ; 0x1
4c: 0a000000 beq 54 <delay+0x30>
50: eafffff8 b 38 <delay+0x14>
54: e89da808 ldmia sp, {r3, fp, sp, pc}
00000058 <led_on>:
58: e1a0c00d mov ip, sp
5c: e92dd800 stmdb sp!, {fp, ip, lr, pc}
60: e24cb004 sub fp, ip, #4 ; 0x4
64: e24dd00c sub sp, sp, #12 ; 0xc
68: e50b0010 str r0, [fp, #-16]
6c: e3a03456 mov r3, #1442840576 ; 0x56000000
70: e2833050 add r3, r3, #80 ; 0x50
74: e50b3014 str r3, [fp, #-20]
78: e3a03456 mov r3, #1442840576 ; 0x56000000
7c: e2833054 add r3, r3, #84 ; 0x54
80: e50b3018 str r3, [fp, #-24]
84: e51b3010 ldr r3, [fp, #-16]
88: e3530004 cmp r3, #4 ; 0x4
8c: 1a000003 bne a0 <led_on+0x48>
90: e51b2014 ldr r2, [fp, #-20]
94: e3a03c01 mov r3, #256 ; 0x100
98: e5823000 str r3, [r2]
9c: ea000005 b b8 <led_on+0x60>
a0: e51b3010 ldr r3, [fp, #-16]
a4: e3530005 cmp r3, #5 ; 0x5
a8: 1a000002 bne b8 <led_on+0x60>
ac: e51b2014 ldr r2, [fp, #-20]
b0: e3a03b01 mov r3, #1024 ; 0x400
b4: e5823000 str r3, [r2]
b8: e51b3018 ldr r3, [fp, #-24]
bc: e3a02000 mov r2, #0 ; 0x0
c0: e5832000 str r2, [r3]
c4: e3a03000 mov r3, #0 ; 0x0
c8: e1a00003 mov r0, r3
cc: e24bd00c sub sp, fp, #12 ; 0xc
d0: 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.
解析:可以看到这个二进制文件,所占用RAM的内存是从地址0开始,到0xd0,转换成十进制,也就是208个字节,最后一部分从comment中是注释信息,不会写入RAM里边去。在RAM中具体体现入下:
1.设置栈sp的起始地址
2.跳转到led_on函数
指令:stm
含义:把多个寄存器的值写入内存
例子:
指令:stmdb sp!, {fp, ip, lr, pc}
解析:假设sp=4096,db是预先减少(Decrement Before)的意思,就是先减少后存入sp开始的地址,而且存取的顺序的依据是:高编号的寄存器存储在高地址。fp, ip, lr, pc 这四个的寄存器编号分别如下所示(ARM编程手册查看)
pc->R15,lr->R14,ip->R12,fp->R11.所以存取的顺序是:pc-lr-ip-fp(与顺序无关)
存储的大体如下,先减后存入:
提示:sp!的含义代表的是,sp会等于执行完此条语句后的值,比如存入四个寄存器,那么得减掉4次,那么sp=4096-16=4080。
感叹后表示执行完语句后sp的值会被改变.
附:
其他形式简单的描述指令的行为,意思分别是过后增加(Increment After)、预先增加(Increment Before)、过后减少(Decrement After)、预先减少(Decrement Before)。
3.读内存读取数据,然后把读取的数据写入多个寄存器(fp, sp, pc)
注意:此时fp、sp、pc的值已经更新,且sp=4096 (每次退出函数之前,sp都会恢复起初设定的值)
4.此时pc开始从0xc执行语句
5.跳转到 delay函数:
开始进行延时,其实就是把一个参数从 100000 减到 0 的操作而已
注意:在上边的语句中,把sp的值减去4,这是因为在地址0x54的时候,还需要把r3的值读取出来,体会一下。
接着执行 ldmia ,也就是退出函数时,存储变量的值,比如pc的值,因为入栈时,pc的值保存了跳转之前的下一条语句的地址,需要回去重新执行,sp 堆栈指针,在进入和退出时的值应该是一样的。
6.点亮另一个LED灯
执行完这个delay函数,pc保存了0x14的值,从0x14地址开始执行函数
其实后边的汇编程序和点亮第一个LED灯的程序思路是一样的,只有一处需要注意一下:
5.看门狗定时器(WATCHDOG TIMER)
看门狗简介:看门狗顾名思义就是帮着你盯住这个系统的运行,防止系统跑飞或者卡死的情况出现,如果在看门狗是一个倒数的定时器,如果在倒数到0之前没有对计数器进行重装载值的重新赋值操作(喂狗),就会触发系统复位。
在第四节中,点亮两个LED灯,他的真正实验结果是,点亮LED1然后延时,延时结束,点亮LED2,我们本来是想让系统进入死循环的,但是长时间在死循环中,没有进行喂狗,系统自动复位,又重复了上面点灯的顺序和步骤。
那如何让系统不自动复位,达到我们想要的结果呢? 关闭开门狗
1.看一看看门狗寄存器的内容(SC32440芯片数据手册)
从手册看出,默认是开启看门狗的,我们只需要往寄存器WTCON中的第0位写0就可以关闭看门狗计时器
小技巧:在程序中如何自动分辨是nor启动还是nand启动
/*自动分辨是nand/nor启动方式*/
思路:因为nand是支持直接读取的,不需要什么格式,而nor可以任意写,但是读取需要特定的格式,若一开始往0地址写入一个值,然后读出来。如果读取的值和写入的值是一致的,说明是nand启动(因为nand可以任意读写)否则是nor启动。
2.这个实验目的是顺序点亮三盏灯
看一下对应的IO口是什么?
需要设置的是:GPF4/5/6 这三个IO口
2.1.在主函数中配置GPF4/5/6 为输出引脚
为了不破坏这个寄存器其他的位,我们可以这样做(假设已经设置:unsigned int *pGPFCON=(unsigned int *)0x56000050;):
①把这几个位清零:*pGPFCON &=~((3<<8)| (3<<10) | (3<<12));
②然后设置为输出:*pGPFCON | =((1<<8) | (1<<10) | (1<<12));
2.2.往GPFDAT寄存器写值,设置IO口输出的电平
3.汇编代码如下,设置栈 sp,自动判别是nand还是nor启动
.text
.global _start
_start:
/*关闭看门狗*/
ldr r0 ,=0x53000000
ldr r1 ,=0
str r1 ,[r0]
/*设置内存:sp 栈*/
/*自动分辨是nand/nor启动方式*/
/*思路:因为nand是支持直接读取的,不需要什么
格式,而nor可以任意写,但是读取需要特定的
格式,若一开始往0地址写入一个值,然后读出来
如果读取的值和写入的值是一致的,说明是nand启动
否则是nor启动*/
mov r1 ,#0 //r1赋值为0
ldr r0 ,[r1] //读取0地址的值,以便后面恢复
str r1 ,[r1] //把0写入0地址里面
ldr r2 ,[r1] //从0地址读取值到r2
cmp r1 ,r2 //判断r1和r2是否相等,相等为nand启动
ldr sp ,=0x40000000+4096 //假设为nor启动
moveq sp, #4096 //相当为nand启动,重新设置sp
streq r0,[r1] //恢复nand中0地址的值
bl main
halt:
b halt
4.C语言代码如下:
void delay(volatile int s) //用于延时,需要传递一个参数
{
while(s--);
}
int main(void) //用于选择点亮哪个led灯,需要一个参数
{
volatile unsigned int *pGPFCON=(volatile unsigned int *)0x56000050;
volatile unsigned int *pGPFDAT=(volatile unsigned int *)0x56000054;
int val=4;
/*配置GPF4/5/6为输出引脚*/
*pGPFCON &=~((3<<8)|(3<<10)|(3<<12));
*pGPFCON |= ((1<<8)|(1<<10)|(1<<12));
/*初始化先熄灭三盏灯*/
*pGPFDAT |=(7<<4);
/*循环点亮三盏led灯*/
while(1)
{
*pGPFDAT &=~(1<<val);
delay(100000);
val++;
if(val==8)
{
val=4;
*pGPFDAT |=(7<<4);
delay(50000);
}
}
return 0;
}
volatile的功能:防止编译器优化
5.Makefile文件内容如下:
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 *.dis
6.传回Linux系统,编译:
编译,使用make命令:
7.把led.bin文件传回window系统 ,使用oflash下载到开发版当中
8.运行程序
运行效果:
三盏LED灯一次点亮,然后熄灭,又依次点亮,重复运行。
提示:这个程序可以烧写到nor中,然后从nor启动,程序可以自动识别是nor启动还是nand启动。
6.优化程序
6.1.为了让程序看起来更人性化,所以可以把上面的程序作如下的修改,运行结果还是不变的:
void delay(volatile int s) //用于延时,需要传递一个参数
{
while(s--);
}
/*
注意:此处的GPFCON相当于刚刚的( *pGPFCON)
现在:GPFCON = 5;的效果就是相当于
往0x56000050这个地址写数值5的意思。
*/
#define GPFCON (*((volatile unsigned int *)0x56000050))
#define GPFDAT (*((volatile unsigned int *)0x56000054))
int main(void) //用于选择点亮哪个led灯,需要一个参数
{
int val=4;
/*配置GPF4/5/6为输出引脚*/
GPFCON &=~((3<<8)|(3<<10)|(3<<12));
GPFCON |= ((1<<8)|(1<<10)|(1<<12));
/*初始化先熄灭三盏灯*/
GPFDAT |=(7<<4);
/*循环点亮三盏led灯*/
while(1)
{
GPFDAT &=~(1<<val);
delay(100000);
val++;
if(val==8)
{
val=4;
GPFDAT |=(7<<4);
delay(50000);
}
}
return 0;
}
6.2.还可以进一步优化,把这些有关于寄存器的宏定义单独放在一个文件里面
然后再使用到这些寄存器的程序,使用#include 就可以了
最终优化如下:
#include "s3c2440_soc.h"
void delay(volatile int s) //用于延时,需要传递一个参数
{
while(s--);
}
/*
注意:此处的GPFCON相当于刚刚的( *pGPFCON)
现在:GPFCON = 5;的效果就是相当于
往0x56000050这个地址写数值5的意思。
*/
int main(void) //用于选择点亮哪个led灯,需要一个参数
{
int val=4;
/*配置GPF4/5/6为输出引脚*/
GPFCON &=~((3<<8)|(3<<10)|(3<<12));
GPFCON |= ((1<<8)|(1<<10)|(1<<12));
/*初始化先熄灭三盏灯*/
GPFDAT |=(7<<4);
/*循环点亮三盏led灯*/
while(1)
{
GPFDAT &=~(1<<val);
delay(100000);
val++;
if(val==8)
{
val=4;
GPFDAT |=(7<<4);
delay(50000);
}
}
return 0;
}