Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1033907
  • 博文数量: 26
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 437
  • 用 户 组: 普通用户
  • 注册时间: 2019-09-08 12:19
个人简介

关于个人介绍,既然你诚心诚意的看了 我就大发慈悲的告诉你 为了防止世界被破坏 为了维护世界的和平 贯彻爱与真实的邪恶 可爱又迷人的反派角色 我们是穿梭在银河的火箭队 白洞,白色的明天在等着我们

文章分类

分类: C/C++

2019-10-11 17:41:31

    在描述了《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应该运行的地址:
点击(此处)折叠或打开
  1.  P1             P2           P3       
  2.  |               |            |
  3.  v               v            v
  4. CPU行       CPU译码       CPU取指(PC)
    上图为CPU的三级流水线:执行,译码,取指。PC总是指向CPU取指的地址。

1、函数调用 返回
点击(此处)折叠或打开
  1. MOV R0, #0
  2. BL test
  3. MOV R1,R0
    当第2行语句执行后,程序进入test函数运行,此时,LR寄存器保存的是第3行的地址,因此,当test执行结束后,我们也希望继续运行第3行地址,所以PC应该直接赋值成LR的值:mov pc, lr

2、系统调用和未定义指令异常返回
点击(此处)折叠或打开
  1. S1
  2. S2
  3. S3
  4. S4
    当执行S1指令的时候,触发了系统调用或者未定义指令(系统调用和未定义还没有执行完成,因此执行指令依然是S1没有更新),此时PC指向S3(PC始终指向正在取指的值),LR的值为PC - 4(ARM模式)。因此,返回的时候,我们希望从S2开始运行,因此,PC应该赋值为:mov pc, lr

3、FIQ和IRQ异常返回
点击(此处)折叠或打开
  1. S1
  2. S2
  3. S3
  4. S4
    当执行S1指令时,发生了中断(中断会等到S1指令执行结束后才响应,因此执行指令和PC都已经更新,为S2和S4),此时PC指向S4,LR的值为PC-4即S3。因此,返回的时候,我们希望程序从S2开始运行,故,PC应该赋值为: subs pc, lr, #4。

4、取指异常返回

点击(此处)折叠或打开

  1. S1
  2. S2
  3. S3
  4. S4
    当执行S1指令时,发生了取指异常(取指异常发生在指令获取阶段,但是这个需要在执行这个错误指令的时候才会触发异常),此时S1就应该是这个取指异常的指令,PC为S3, LR为PC - 4即S2。因此,我们希望异常发生返回进行重新取指,所以PC应该赋值为S1的值即:subs pc, lr, #4。

5、数据访问异常返回
点击(此处)折叠或打开
  1. S1
  2. S2
  3. S3
  4. S4
    当执行S1指令发生数据访问异常的时候,访问异常是发生在指令结束后,此时正在执行的指令为S2,PC为S4, LR为PC-4即S3。因此,我们希望返回重新去取指访问数据,所以PC应该赋值为S1的值即:subs pc, lr, #8

    上述说完了LR和PC关系和取值,下面继续说关于FP的堆栈回溯功能, 如图:
    上图是一个栈内容,函数调用栈为:

点击(此处)折叠或打开

  1. static int b(void)
  2. {
  3.     return 0;
  4. }

  5. static int a(void)
  6. {
  7.     b();
  8.     return 0;
  9. }

  10. static int t(void)
  11. {
  12.     a();
  13.     return 0;
  14. }
    通过上两图,我们假设在函数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。
因此,崩溃时候的调用栈是:
点击(此处)折叠或打开
  1. b函数
  2.     0x300 (a函数)
  3.         0x500(t函数)

    不管函数调用多少层,都可以用同样方法,一直找到最上层调用者。堆栈回溯的前提是:编译的时候 不能禁用FP功能(gcc编译的时候不要添加-fomit-frame-pointer参数,否则堆栈回溯会有问题)。

    我们从上面已经知道了寄存器的常用方法,下面我们通过一段hello程序来进一步说明,程序源码如下:
点击(此处)折叠或打开
  1. #include <stdio.h>

  2. static int main_test(int number)
  3. {
  4.     int i = 1000;

  5.     for (;> 0; i--)
  6.         number += i % 10;

  7.     return number;
  8. }

  9. int main (int argc, char *argv[])
  10. {
  11.     printf ("Hello World\n");

  12.     return 0;
  13. }
执行: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程序转换成汇编后的工作情况,如下:

点击(此处)折叠或打开

  1. Disassembly of section .plt:

  2. 82c0:   e28fc600    add ip, pc, #0, 12
  3.     82c4:   e28cca08    add ip, ip, #8, 20  ; 0x8000
  4.     82c8:   e5bcf324    ldr pc, [ip, #804]! ; 0x324
  5.     82cc:   e28fc600    add ip, pc, #0, 12
  6.     82d0:   e28cca08    add ip, ip, #8, 20  ; 0x8000
  7.     82d4:   e5bcf31c    ldr pc, [ip, #796]! ; 0x31c
  8.     82d8:   4778        bx  pc
  9.     82da:   46c0        nop         ; (mov r8, r8)
  10.     82dc:   e28fc600    add ip, pc, #0, 12
  11.     82e0:   e28cca08    add ip, ip, #8, 20  ; 0x8000
  12.     82e4:   e5bcf310    ldr pc, [ip, #784]! ; 0x310
  13.     82e8:   e28fc600    add ip, pc, #0, 12
  14.     82ec:   e28cca08    add ip, ip, #8, 20  ; 0x8000
  15.     82f0:   e5bcf308    ldr pc, [ip, #776]! ; 0x308

  16. 000083cc <main_test>:
  17.     83cc: e52db004 push {fp} ; (str fp, [sp, #-4]!)
  18.     83d0: e28db000 add fp, sp, #0
  19.     83d4: e24dd014 sub sp, sp, #20
  20.     .............
  21.     8434: e3530000 cmp r3, #0
  22.     8438: caffffea bgt 83e8 <main_test+0x1c>
  23.     843c: e51b3010 ldr r3, [fp, #-16]
  24.     8440: e1a00003 mov r0, r3         # 返回值赋值给r0
  25.     8444: e28bd000 add sp, fp, #0
  26.     8448: e8bd0800 ldmfd {fp}
  27.     844c: e12fff1e bx lr              # 返回main函数

  28. 00008450 <main>:
  29.     8450: e92d4800 push {fp, lr}  # 将lr和fp压栈
  30.     8454: e28db004 add fp, sp, #4  # 设置新的fp地(这里的fp位置能和上面描述有所不同,但原理一样)
  31.     8458: e24dd008 sub sp, sp, #8  # 开辟局部变量地址空间

  32.     845c: e50b0008 str r0, [fp, #-8# 给局部变量赋值 处置,这里fp -8 是argc
  33.     8460: e50b100c str r1, [fp, #-12# 给局部变量赋值 处置,这里fp -12 是argv

  34.     8464: e30804d4 movw r0, #34004 ; 0x84d4  # 初始化函数的第一个传参,r0
  35.     8468: e3400000 movt r0, #0
  36.     846c: ebffff93 bl 82c0 <_init+0x20>     # 这里利用0x82c0 间接跳转到main_test函数
  37.     8470: e3a03000 mov r3, #0
  38.     8474: e1a00003 mov r0, r3                # 将返回值 赋值给r0,相当于return 0;
  39.     8478: e24bd004 sub sp, fp, #4            # 还原栈
  40.     847c: e8bd8800 pop {fp, pc}              # 弹出 上一个函数的fp和lr返回地址到pc

     以上只对main函数的调用情况做出了说明。本章的内容,以后再内核的上下文切换、内核异常等问题的时候会用到。上下文切换在以后会单独讲解。
 


阅读(8179) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

BugMan2019-10-23 17:33:26

注:在arm模式下,fp默认为r11, ip为r12

HongqiangXu2019-10-21 16:10:01

今天我 寒夜里看雪飘过

怀着冷却了的心窝漂远方

风雨里追赶 雾里分不清影踪