在描述了《
Linux内核之execve函数》之后,我发现要真正的理解start_thread函数的作用,我不得不提一下C语言是怎么转换成汇编的,以及寄存器在汇编语言中的操作规范和原则。本文主要简单说明一下寄存器在ARM 32位中的一些使用基本常识(64位 ARM以后介绍)。
首先ARM有许多寄存器(这里说的ARM 32位,指的是arm cortex A系列),如下图:
从图中可以看出(图选自于《cortex_A_series_PG.pdf》),32位arm的R0 - R7为低端寄存器(Thumb16模式下,只能使用R0-R7,R13,R14,R15这几个寄存器),这在所有ARM的工作模式下是共享的(ARM有7种工作模式:Usr, Sys, FIQ, IRQ, ABT, SVC, UND,其中MON和HYP我们暂时不计入考虑,大部分操作系统工作于SVC模式下,应用工作在Usr模式下面)。下面我们来看看各个寄存器的作用。
R0寄存器: 通常用于函数传参(参数1)或者普通寄存器或者函数返回值。
R1寄存器: 通常用于函数传参(参数2)或者普通寄存器。
R2寄存器: 通常用于函数传参(参数3)或者普通寄存器。
R3寄存器: 通常用于函数传参(参数4)或者普通寄存器。
R7寄存器: 系统调用时,存放系统调用号,有时也用于作为FP使用。FP又叫frame pointer即栈基指针,主要在函数中保存当前函数的栈起始位置,用于堆栈回溯。
R13寄存器:R13又名SP,即栈指针寄存器,主要用于指向当前程序栈顶,配合指令pop/push等。栈主要用于存放局部变量,保存函数间调用的关键寄存器,如LR。SP可以是向上增长也可以是向下增长,通常我们ARM采用的向下增长模式,下图为一个函数调用时,栈的排布情况。
R14寄存器:R14又名LR,即链接寄存器,主要用于存放函数的返回地址,即当前函数返回时,知道自己该回到哪儿去继续运行,通常这个是和BL/BLX/CALL指令搭配,这几个指令被调用后,默认会自动将当前调用指令的下一条指令地址保存到LR寄存器当中。
R15寄存器:R15又名PC,即程序寄存器,主要用于存放CPU取指的地址,记住是取指地址,不是当前运行地址。目前,ARM是三级流水线,因此,当CPU在执行S指令的时候,PC指向的是S+2指令。但是当手动向PC赋值,则是让CPU跳转到赋入的值 所代表的地址去运行。
注:通常PC指针指向的地址都是4字节对齐,即地址的[1:0]位总是为0,这也是我们说的ARM模式。现在很多CPU都支持混合编码即同时支持ARM指令和Thumb指令,因此为了区分Thumb指令,ARM将[0]位设置成1,即地址最低位如果是1,表示当前指令是Thumb指令,否则为ARM指令。
Thumb(16)指令占用的空间通常比ARM指令少,但是ARM指令运行的效率通常要比Thumb更高,Thumb模式到ARM模式可以通过带X的跳转进行切换,如BLX, BX跳转指令(Thumb分为Thumb16和Thumb32)。
上面说了寄存器的情况,下面说说在不同模式下,程序返回时,CPU应该运行的地址:
点击(此处)折叠或打开
-
P1 P2 P3
-
| | |
-
v v v
-
CPU执行 CPU译码 CPU取指(PC)
上图为CPU的三级流水线:执行,译码,取指。PC总是指向CPU取指的地址。
1、函数调用 返回
点击(此处)折叠或打开
-
MOV R0, #0
-
BL test
-
MOV R1,R0
当第2行语句执行后,程序进入test函数运行,此时,LR寄存器保存的是第3行的地址,因此,当test执行结束后,我们也希望继续运行第3行地址,所以PC应该直接赋值成LR的值:mov pc, lr
2、系统调用和未定义指令异常返回
点击(此处)折叠或打开
当执行S1指令的时候,触发了系统调用或者未定义指令(系统调用和未定义还没有执行完成,因此执行指令依然是S1没有更新),此时PC指向S3(PC始终指向正在取指的值),LR的值为PC - 4(ARM模式)。因此,返回的时候,我们希望从S2开始运行,因此,PC应该赋值为:mov pc, lr
3、FIQ和IRQ异常返回
点击(此处)折叠或打开
当执行S1指令时,发生了中断(中断会等到S1指令执行结束后才响应,因此执行指令和PC都已经更新,为S2和S4),此时PC指向S4,LR的值为PC-4即S3。因此,返回的时候,我们希望程序从S2开始运行,故,PC应该赋值为: subs pc, lr, #4。
4、取指异常返回
当执行S1指令时,发生了取指异常(取指异常发生在指令获取阶段,但是这个需要在执行这个错误指令的时候才会触发异常),此时S1就应该是这个取指异常的指令,PC为S3, LR为PC - 4即S2。因此,我们希望异常发生返回进行重新取指,所以PC应该赋值为S1的值即:subs pc, lr, #4。
5、数据访问异常返回
点击(此处)折叠或打开
当执行S1指令发生数据访问异常的时候,访问异常是发生在指令结束后,此时正在执行的指令为S2,PC为S4, LR为PC-4即S3。因此,我们希望返回重新去取指访问数据,所以PC应该赋值为S1的值即:subs pc, lr, #8
上述说完了LR和PC关系和取值,下面继续说关于FP的堆栈回溯功能, 如图:
上图是一个栈内容,函数调用栈为:
-
static int b(void)
-
{
-
return 0;
-
}
-
-
static int a(void)
-
{
-
b();
-
return 0;
-
}
-
-
static int t(void)
-
{
-
a();
-
return 0;
-
}
通过上两图,我们假设在函数b里面发生了异常,比如是SEGV异常。
1、通过读取寄存器R13和R7我们可以得到R13 = 0x0800(栈顶), R7 = 0x1000。
2、通过栈的布局情况我们了解到,FP + 4为上一个函数的返回地址LR。在b崩溃的时候,LR的值是*0X1004 = 0x300,即这个
0x300就是a函数调用b函数的时候的下一条指令地址。
3、
通过栈的布局情况我们了解到,fp则存放的是上一个函数fp的地址。因此,我们可以拿到a函数的fp地址:*0x1000 = 0x2000。
4、重复2 3 步骤,我们可以获取到t
函数调用a函数的时候的下一条指令地址:0x500。
因此,崩溃时候的调用栈是:
点击(此处)折叠或打开
-
b函数
-
0x300 (a函数)
-
0x500(t函数)
不管函数调用多少层,都可以用同样方法,一直找到最上层调用者。堆栈回溯的前提是:编译的时候 不能禁用FP功能(gcc编译的时候不要添加-fomit-frame-pointer参数,否则堆栈回溯会有问题)。
我们从上面已经知道了寄存器的常用方法,下面我们通过一段hello程序来进一步说明,程序源码如下:
点击(此处)折叠或打开
-
#include <stdio.h>
-
-
static int main_test(int number)
-
{
-
int i = 1000;
-
-
for (;i > 0; i--)
-
number += i % 10;
-
-
return number;
-
}
-
-
int main (int argc, char *argv[])
-
{
-
printf ("Hello World\n");
-
-
return 0;
-
}
执行:arm-linux-gnueabihf-gcc -marm -o hello-2 hello-2.c -fno-omit-frame-pointer生成hello可执行文件(ELF格式),再使用arm-linux-gnueabihf-objdump -d hello > hello.s得到对应的ARM 32位汇编代码。下面,我们将看看一个C程序转换成汇编后的工作情况,如下:
-
Disassembly of section .plt:
-
-
82c0: e28fc600 add ip, pc, #0, 12
-
82c4: e28cca08 add ip, ip, #8, 20 ; 0x8000
-
82c8: e5bcf324 ldr pc, [ip, #804]! ; 0x324
-
82cc: e28fc600 add ip, pc, #0, 12
-
82d0: e28cca08 add ip, ip, #8, 20 ; 0x8000
-
82d4: e5bcf31c ldr pc, [ip, #796]! ; 0x31c
-
82d8: 4778 bx pc
-
82da: 46c0 nop ; (mov r8, r8)
-
82dc: e28fc600 add ip, pc, #0, 12
-
82e0: e28cca08 add ip, ip, #8, 20 ; 0x8000
-
82e4: e5bcf310 ldr pc, [ip, #784]! ; 0x310
-
82e8: e28fc600 add ip, pc, #0, 12
-
82ec: e28cca08 add ip, ip, #8, 20 ; 0x8000
-
82f0: e5bcf308 ldr pc, [ip, #776]! ; 0x308
-
000083cc <main_test>:
-
83cc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
-
83d0: e28db000 add fp, sp, #0
-
83d4: e24dd014 sub sp, sp, #20
-
.............
-
8434: e3530000 cmp r3, #0
-
8438: caffffea bgt 83e8 <main_test+0x1c>
-
843c: e51b3010 ldr r3, [fp, #-16]
-
8440: e1a00003 mov r0, r3 # 返回值赋值给r0
-
8444: e28bd000 add sp, fp, #0
-
8448: e8bd0800 ldmfd {fp}
-
844c: e12fff1e bx lr # 返回main函数
-
-
00008450 <main>:
-
8450: e92d4800 push {fp, lr} # 将lr和fp压栈
-
8454: e28db004 add fp, sp, #4 # 设置新的fp地址(这里的fp位置可能和上面描述有所不同,但原理一样)
-
8458: e24dd008 sub sp, sp, #8 # 开辟局部变量地址空间
-
-
845c: e50b0008 str r0, [fp, #-8] # 给局部变量赋值 处置,这里fp -8 是argc
-
8460: e50b100c str r1, [fp, #-12] # 给局部变量赋值 处置,这里fp -12 是argv
-
-
8464: e30804d4 movw r0, #34004 ; 0x84d4 # 初始化函数的第一个传参,r0
-
8468: e3400000 movt r0, #0
-
846c: ebffff93 bl 82c0 <_init+0x20> # 这里利用0x82c0 间接跳转到main_test函数
-
8470: e3a03000 mov r3, #0
-
8474: e1a00003 mov r0, r3 # 将返回值 赋值给r0,相当于return 0;
-
8478: e24bd004 sub sp, fp, #4 # 还原栈
-
847c: e8bd8800 pop {fp, pc} # 弹出 上一个函数的fp和lr返回地址到pc
-
以上只对main函数的调用情况做出了说明。本章的内容,以后再内核的上下文切换、内核异常等问题的时候会用到。上下文切换在以后会单独讲解。
阅读(8237) | 评论(2) | 转发(0) |