Skip to content

第三次

约 6382 字大约 21 分钟

2024-7-26

之前写的都不太好,应该加个周计划的内容然后看能完成多少,况且距离第二周的已经相隔了 40 天了,原因暂且不表

然后之前其实是有第三周的,也算学了点有用的东西,但都跟开发有关,就是学习了基础的 gcc g++ 还有 gdb 调试以及写 CMakeLists.txt 文件(注意大小写严格)

计划

依然是向着逆向方面学,之前把 16 位的 x86 汇编看完了,arm 汇编依旧没怎么看,arm 方面的我选择不专门去看,看到不会的在临时看,所以就是逆向工程权威指南这本书

再然后就是因为之前杨大哥有让我们搞过固件分析,当时太蠢了啥都不懂,现在有思路了,我先是看了下主流的路由器板子,就是认识下 cpu、闪存、内存、供电等。接下来就是看一些主流的固件系统。然后看了一篇使用 IDA 逆向程序的帖子,了解了一些用法之后,开始看《IDA Pro 权威指南》。还有这本《逆向工程权威指南》。

  1. ARM汇编基础花费周一周二看完,看完了之后就能基本无障碍读《逆向工程权威指南》
  2. 《IDA Pro 权威指南》周三周四看三章第一部分内容
  3. 《逆向工程权威指南》周五周六看三章第一部分内容
  4. 其余时间会去补操作系统的一些知识,然后在这期间写周报

然后我认为也得看一下小迪的课程,以我目前的水平在逆向方面做不了什么实质的东西

ARM 汇编

重要

没有简介了,arm 不需要简介

2 ARM 数据类型和寄存器

  1. 与高级语言类似,ARM 支持对不同数据类型的操作。

    ldr = 加载字
    ldr h = 加载无符号半字
    ldr sh = 加载有符号半字
    ldr b = 加载无符号字节
    ldr sb = 加载有符号字节
     32 半字 16 字节 8
    str = 存储字
    str h = 存储无符号半字
    str sh = 存储有符号半字
    str b = 存储无符号字节
    str sb = 存储有符号字节
  2. 字节顺序有所不同,x86 架构分大小端查看,ARM 架构在版本 3 之前是小端,之后是双端,这意味着它具有允许可切换字节序的设置。例如,在 ARMv6 上,指令是固定的小端,数据访问可以是小端或大端,由程序状态寄存器 (CPSR) 的第 9 位(E 位)控制。

  3. 寄存器数量取决于 ARM 版本,根据 ARM 参考手册,共有 30 个通用 32 位寄存器。前 16 个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用。

    注意

    以上所说的内容,基于 ARMv6-M 和 ARMv7-M 的处理器除外。

  4. 16个通用寄存器其中的几个与 x86 有区别

    • LR(链接寄存器)保存函数调用的下一个内存地址,方便子程序返回。x86 中没有专门的用于函数调用的寄存器,只有 call ret 组合来实现这个功能,call 的时候就会把下一个地址压入栈。

    • PC(程序计数器)在 ARM 状态下,此大小始终为 4 个字节,在 THUMB 模式下,则为 2 个字节。执行分支指令时,PC 保存目标地址。在执行过程中,在 ARM 状态下,PC 存储当前指令的地址加 8(两个 ARM 指令),在 Thumb(v1) 状态下,PC 存储当前指令的地址加 4(两个 Thumb 指令)。这与 x86 不同,在 x86 中,PC 始终指向下一个要执行的指令。

    • CPSR(当前程序状态寄存器)类似于 x86 中的标志寄存器

      标志描述
      N(Negative)如果指令的结果产生负数,则启用。
      Z(Zero)如果指令的结果产生零值,则启用。
      C(Carry)如果指令的结果产生了一个需要第33位才能完全表示的值,则启用。
      V(Overflow)如果指令的结果产生了一个在32位补码中无法表示的值,则启用。
      E(Endian-bit)ARM可以操作于小端模式或大端模式。此位设置为0表示小端模式,或设置为1表示大端模式。
      T(Thumb-bit)如果处于Thumb状态,则此位被设置;如果处于ARM状态,则此位被禁用。
      M(Mode-bits)这些位指定当前的特权模式(用户模式USR、系统模式SVC等)。
      J(Jazelle)第三执行状态,允许一些ARM处理器在硬件中执行Java字节码。

3 ARM 指令集

  1. ARM 处理器有两种主要的运行状态 ARM 和 THUMB。这些状态与特权级别无关。这两个状态之间的主要区别在于指令集,ARM 状态下的指令始终为 32 位,而 Thumb 状态下的指令为 16 位(但可以是 32 位)。需要知道在哪些地方使用Thumb代替ARM。引入Thumb增强指令集之后,还需要了解到自己的设备支持的是哪一种,并进行相应的调整。

  2. ARM 和 Thumb 之间的区别:

    • 条件执行:ARM 状态下的所有指令都支持条件执行。某些 ARM 处理器版本允许使用 IT 指令在 Thumb 中进行条件执行。条件执行可提高代码密度,因为它减少了要执行的指令数量并减少了昂贵的分支指令数量。
    • 32 位 ARM 和 Thumb 指令:32 位 Thumb 指令具有 .w 后缀。
    • 桶形移位器是 ARM 模式的另一个独特功能。它可用于将多条指令缩减为一条。例如,您无需使用两条指令进行乘法运算(将寄存器乘以 2,然后使用 MOV 将结果存储到另一个寄存器中),而是可以使用左移 mov R1, R0, R0, LSL#1
  3. 切换处理器的执行状态以下两个条件必须满足:

    • 我们可以使用分支指令BX(分支和交换)或BLX(分支,链接和交换)并将目标寄存器的最低有效位设置为1。可以通过添加1的偏移来实现,如0x5530+ 1。这不会因为指令要么是2字节,要么是4字节对齐而产生对齐问题。因为处理器将忽略最低有效位。

    • 如果当前程序状态寄存器中的T位置1,处于Thumb模式。

  4. 指令集

    指令描述指令描述
    MOV移动数据EOR位运算异或
    MVN移动并取反LDR加载
    ADD加法STR存储
    SUB减法LDM多重加载
    MUL乘法STM多重存储
    LSL逻辑左移PUSH压入栈
    LSR逻辑右移POP从栈中弹出
    ASR算术右移B跳转
    ROR右旋转BL分支并链接
    CMP比较BX分支并交换
    AND位运算与BLX分支并链接并交换
    ORR位运算或SWI/SVC系统调用

4 内存指令:加载和存储

提示

  • ARM 使用加载-存储模型进行内存访问,这意味着只有加载/存储(LDR 和 STR)指令才能访问内存。虽然在 x86 上,大多数指令都允许直接对内存中的数据进行操作,但在 ARM 上,数据必须先从内存移到寄存器中才能进行操作。这意味着在 ARM 上增加特定内存地址的 32 位值需要三种类型的指令(加载、增量和存储),首先将特定地址的值加载到寄存器中,在寄存器内增加它,然后将其从寄存器存储回内存。

  • 注意 ldr 是右值赋左值,str 是右值赋左值

  • 注意前后索引的计算顺序,最重要的是多看多熟悉

4.1 基本示例
LDR R2, [R0]	@ [R0] - origin address is the value found in R0.
STR R2, [R1]    @ [R1] - destination address is the value found in R1.
#LDR 用于从内存加载数据到寄存器。
STR 用于将寄存器中的数据存储到内存中

👆很经典代码块直接说清楚 LDR 和 STR 关系

假设:

  • R0 中的值是 0x1000,内存地址 0x1000 的内容是 42
  • R1 中的值是 0x2000

执行这两条指令后:

  • LDR R2, [R0]42 加载到 R2 中。
  • STR R2, [R1]42 存储到内存地址 0x2000 中。
4.1.1 立即数作为偏移寻址
var1: .word 3  /* 在内存中定义变量1,值为3 */
var2: .word 4  /* 在内存中定义变量2,值为4 */

.text          /* 代码段开始 */
.global _start /* 声明全局入口点 */

_start:
    ldr r0, adr_var1  @ 加载 var1 的地址到 R0
    ldr r1, adr_var2  @ 加载 var2 的地址到 R1
    ldr r2, [r0]      @ 从 R0 指向的地址加载值 (0x03) 到 R2
    str r2, [r1, #2]  @ 使用偏移量,将 R2 中的值 (0x03) 存储到 R1 + 2 的地址
    str r2, [r1, #4]! @ 使用前索引模式,将 R2 中的值 (0x03) 存储到 R1 + 4 的地址,同时更新 R1
                   #👆感叹号是更新 r1 的意思
    ldr r3, [r1], #4  @ 使用后索引模式,从 R1 指向的地址加载值到 R3,同时更新 R1
    bkpt              @ 设置断点

👆前后索引寻址的区别

4.1.2 寄存器的值作为偏移寻址
var1: .word 3  /* 在内存中定义变量1,值为3 */
var2: .word 4  /* 在内存中定义变量2,值为4 */

.text          /* 代码段开始 */
.global _start /* 声明全局入口点 */

_start:
    ldr r0, adr_var1  @ 加载 var1 的地址到 R0
    ldr r1, adr_var2  @ 加载 var2 的地址到 R1
    ldr r2, [r0]      @ 从 R0 指向的地址加载值 (0x03) 到 R2
    str r2, [r1, r2]  @ 使用偏移量,将 R2 中的值 (0x03) 存储到 R1 + R2 的地址
    str r2, [r1, r2]! @ 使用前索引模式,将 R2 中的值 (0x03) 存储到 R1 + R2 的地址,同时更新 R1
    ldr r3, [r1], r2  @ 使用后索引模式,从 R1 指向的地址加载值到 R3,同时更新 R1
    bx lr              @ 返回

👆前后索引寻址的区别

4.1.3 移位寄存器作为偏移
var1: .word 3  /* 在内存中定义变量1,值为3 */
var2: .word 4  /* 在内存中定义变量2,值为4 */

.text          /* 代码段开始 */
.global _start /* 声明全局入口点 */

_start:
    ldr r0, adr_var1         @ 加载 var1 的地址到 R0
    ldr r1, adr_var2         @ 加载 var2 的地址到 R1
    ldr r2, [r0]             @ 从 R0 指向的地址加载值 (0x03) 到 R2
    str r2, [r1, r2, LSL#2]  @ 使用偏移量,将 R2 中的值 (0x03) 存储到 R1 + (R2 << 2) 的地址
    str r2, [r1, r2, LSL#2]! @ 使用前索引模式,将 R2 中的值 (0x03) 存储到 R1 + (R2 << 2) 的地址,同时更新 R1
    ldr r3, [r1], r2, LSL#2  @ 使用后索引模式,从 R1 指向的地址加载值到 R3,同时更新 R1
    bkpt                      @ 设置断点
4.2 LDR 用于PC相对寻址
.section .text
.global _start

_start:
   ldr r0, =jump        /* load the address of the function label jump into R0 */
   ldr r1, =0x68DB00AD  /* load the value 0x68DB00AD into R1 */
jump:
   ldr r2, =511         /* load the value 511 into R2 */ 
   bkpt

为什么我们有时需要使用这种语法来使用一个指令将32位常量移到一个的寄存器中呢,这是因为ARM一次 只能加载一个8位的值。什么?要理解为什么,你需要知道ARM上的立即数是如何处理的。

4.3 ARM 中立即数的使用
.section .text
.global _start

_start:
	mov r0, #256
	add r0, #255
	ldr r1, =511    /* load 511 from the literal pool using LDR */
	bkpt
#最终 r0 结果是511,r1 结果也是511

5 载入/存储多个值

有时候一次加载或者存储多个值会更有效率。因此我们会使用 LDM(load multiple) 指令和 STM(store multiple) 指令,这些指令有一些在访问初始地址方式上有所不同

.data

array_buff:
 .word 0x00000000             /* array_buff[0] */
 .word 0x00000000             /* array_buff[1] */
 .word 0x00000000             /* array_buff[2]. This element has a relative address of array_buff+8 */
 .word 0x00000000             /* array_buff[3] */
 .word 0x00000000             /* array_buff[4] */

.text
.global _start

_start:
 adr r0, words+12             /* r0 = &words[3] (0x00000003) */
 ldr r1, array_buff_bridge    /* r1 = &array_buff[0] */
 ldr r2, array_buff_bridge+4  /* r2 = &array_buff[2] */
 ldm r0, {r4,r5}              /* 从 r0 加载 words[3] 和 words[4] 到 r4 和 r5 */
 stm r1, {r4,r5}              /* 将 r4 和 r5 的值存储到 array_buff[0] 和 array_buff[1] */
 ldmia r0, {r4-r6}            /* 从 r0 加载 words[3], words[4], words[5] 到 r4, r5, r6 */
 stmia r1, {r4-r6}            /* 将 r4, r5, r6 存储到 array_buff[0], array_buff[1], array_buff[2] */
 ldmib r0, {r4-r6}            /* 逐字节加载 words[4], words[5], words[6] 到 r4, r5, r6 */
 stmib r1, {r4-r6}            /* 逐字节存储 r4, r5, r6 到 array_buff[1], array_buff[2], array_buff[3] */
 ldmda r0, {r4-r6}            /* 从 r0 加载 words[3], words[2], words[1] 到 r6, r5, r4 */
 ldmdb r0, {r4-r6}            /* 从 r0 加载 words[2], words[1], words[0] 到 r6, r5, r4 */
 stmda r2, {r4-r6}            /* 存储 r6, r5, r4 到 array_buff[2], array_buff[1], array_buff[0] */
 stmdb r2, {r4-r5}            /* 存储 r5, r4 到 array_buff[1], array_buff[0] */
 bx lr
 
 words:
 .word 0x00000000             /* words[0] */
 .word 0x00000001             /* words[1] */
 .word 0x00000002             /* words[2] */
 .word 0x00000003             /* words[3] */
 .word 0x00000004             /* words[4] */
 .word 0x00000005             /* words[5] */
 .word 0x00000006             /* words[6] */

array_buff_bridge:
 .word array_buff             /* address of array_buff, or in other words - array_buff[0] */
 .word array_buff+8           /* address of array_buff[2] */

👆主要目的就是要记住 adr(加载一个标签的地址到寄存器)、ldr、ldm、stm、ldmia、stmib、ldmda、ldmdb、stmda、stmdb

但说实话暂时为了快速过一遍百分百是记不住的就只是看了一遍理解了一下等到后面看到的时候再说吧😂。

6 出栈和入栈

.text
.globa _start

_start:
	mov r0, #3
	mov r1. #4
	push {r0, r1}
	pop {r2, r3}
	stmdb sp!, {r0, r1}
	ldmia sp!, {r4, r5}

👆和 x86 差不多就是可以一次性 push & pop 好几个,至于 push 和 pop 的顺序都是先右再左

7 ARM 条件执行指令 && Thumb模式下的条件执行 && 分支指令

下面列出了常用的条件,以及他们影响的CPSR寄存器的条件位:

  • Z (Zero Flag): 当结果为零时设置。
  • N (Negative Flag): 当结果为负时设置。
  • C (Carry Flag): 在无符号运算中,当发生进位时设置。
  • V (Overflow Flag): 当有符号运算的结果溢出时设置。
Condition Code含义Status of Flags
EQ等于Z == 1
NE不等于Z == 0
GT有符号大于(Z == 0) && (N == V)
LT有符号小于N != V
GE有符号大于或等于N == V
LE有符号小于或等于(Z == 1)
CS or HS无符号大于或相等(进位设置)C == 1
CC or LO无符号小于(进位清除)C == 0
MI负数(或负值)N == 1
PL正数(或正值)N == 0
AL始终执行
NV从不执行
VS有符号溢出V == 1
VC没有有符号溢出V == 0
HI无符号大于(C == 1) && (Z == 0)
LS无符号小于或相等(C == 0)

例子

.global main

main:
        mov     r0, #2          /* 设置初始变量 r0 为 2 */
        cmp     r0, #3          /* 将 r0 与数字 3 进行比较,负标志位被设置为 1 */
        addlt   r0, r0, #1      /* 如果 r0 小于 3,则将 r0 增加 1 */
        cmp     r0, #3          /* 再次将 r0 与数字 3 进行比较,零标志位被设置为 1 */
        addlt   r0, r0, #1      /* 如果 r0 小于 3,则将 r0 再次增加 1 */
        bx      lr              /* 返回到调用者 */
7.1 Thumb 中的条件执行

在前面我们说过存在不同版本的Thumb的情况,具体来说,有允许条件执行的Thumb版本(Thumb-2)。某些ARM处理器的版本支持IT指令,该指令最多可以在Thumb状态下有条件的执行4条指令。

语法:IT {x {y {z}}} cond

  • cond规定了执行IT语句块里的第一条指令需要满足的条件

  • x规定了执行的IT语句块中第二条指令需要满足的条件

  • y规定了执行IT语句块里的第三条指令需要满足的条件

  • z 规定了执行IT语句块里的第四条指令需要满足的条件

    IT指令集的结构是: “IF-Then-(Else)”,它的语法结构由两个字母构成:

  • IT代表If-Then(下一条指令是条件指令)

  • TT代表If-Then-Then(接下来的两条指令是条件指令)

  • ITE代表 If-Then-Else(接下来的两条指令是条件指令)

  • ITTE代表 If-Then-Then-Else (接下来的三条指令是条件指令)

  • ITTEE代表 If-Then-Then-Else-Else (接下来的四条指令是条件指令)

    IT块中的每条指令必须指定一个条件后缀,该条件后缀可以是相同的,也可以是在逻辑相反的。 这意味着,如果使用了ITE,第一和第二指令(If-Then)必须具有相同的条件后缀,而第三条指令(else语句)必须和前面两条语句逻辑相反。

    ITTE   NE           ; 下三条指令是条件执行的  
    ANDNE  R0, R0, R1   ; 如果上一条指令的结果不为零(NE),则执行AND操作,但ANDNE不更新条件标志  
    ADDSNE R2, R2, #1   ; 如果上一条指令的结果不为零(NE),则执行ADDS(加且设置状态标志)操作  
    MOVEQ  R2, R3       ; 如果上一条指令的结果为零(EQ),则执行MOV操作  
      
    ITE    GT           ; 下两条指令是条件执行的  
    ADDGT  R1, R0, #55  ; 如果比较结果为大于(GT),则执行加法操作  
    ADDLE  R1, R0, #48  ; 如果比较结果不大于(LE,即不是GT),则执行另一个加法操作  
      
    ; 注意:这里我假设了ITTEE是类似于ITT的扩展,但实际上ARM中不存在ITTEE指令。  
    ; 以下是根据你的假设(如果ITTEE存在且意味着两个THEN和两个ELSE)翻译的,但请记住这是虚构的。  
    ITTEE  EQ           ; 假设存在,表示接下来的四条指令是条件执行的,基于上一个比较的结果是否等于(EQ)  
    MOVEQ  R0, R1       ; 如果等于(EQ),则执行MOV操作  
    ADDEQ  R2, R2, #10  ; 如果等于(EQ),则执行加法操作  
    ANDNE  R3, R3, #1   ; 如果不等于(NE),则执行AND操作(注意这里的逻辑与ITTEE的假设不完全一致,因为ITTEE应该还有两个ELSE分支)  
    ; 由于ITTEE不是真实存在的指令,我将不提供完整的ELSE分支,仅翻译前两个条件执行指令  
      
    ; 由于ARM指令集中IT块的最后一条指令只能是分支或中断指令,这里使用了一个分支指令作为示例  
    ; 注意:下面的BNE.W是假设它可以出现在IT块的最后,尽管在某些ARM架构中IT块的使用可能有更严格的限制  
    BNE.W  dloop        ; 如果上一条指令的结果不为零(NE),则跳转到dloop标签处

    下面是与条件代码相反的内容

    Condition Code含义Opposite CodeOpposite Meaning
    EQ等于NE不等于
    HS (or CS)无符号大于或相等(进位设置)LO (or CC)无符号小于(进位清除)
    MI负数PL正数或零
    VS有符号溢出VC没有有符号溢出
    HI无符号大于LS无符号小于或相等
    GE有符号大于或等于LT有符号小于
    GT有符号大于LE有符号小于或等于
    AL (or omitted)始终执行对于 AL 没有反义条件
    .syntax unified    @ 这是重要的!指定汇编语法为统一语法,允许同时使用 ARM 和 Thumb 指令。
    .text
    .global _start
    
    _start:
        .code 32          @ 切换到 ARM 状态
        add r3, pc, #1   @ 将程序计数器(PC)加1的值加到 R3 中
        bx r3             @ 跳转并交换到 R3 中的地址 -> 切换到 Thumb 状态,因为 LSB = 1
    
        .code 16          @ 切换到 Thumb 状态
        cmp r0, #10      @ 比较 R0 和 10
        ite eq           @ 如果 R0 等于 10...
        addeq r1, #2     @ ... 则 R1 = R1 + 2
        addne r1, #3     @ ... 否则 R1 = R1 + 3
        bkpt              @ 触发断点

    .code32

    示例代码以arm状态开始, 第一条指令将PC加1中指定的地址添加到R3,然后跳转到R3中的地址。 这将导致切换到Thumb状态,因为LSB(最低有效位)为1,因此没有4字节对齐。

    .code 16

    在Thumb状态下,我们首先将R0和 #10进行比较,结果是 N = 1,然后我们使用if-Than-else块,这个块会跳过 addeq指令,因为Z位并没有被设置,而addne将被执行,因为结果不等于 10

7.2 分支执行

分支 又称为跳转,使得我们可以跳转到另外一个代码段。当我们需要跳过或者重复代码块,到特定的位置的方法的时候,这将非常有用。这种情况下,最好的示例是IF语句或者一个循环,所以,首先来看一下IF语句:

.global main

main:
        mov     r1, #2     /* 设置初始变量 a */
        mov     r2, #3     /* 设置初始变量 b */
        cmp     r1, r2     /* 比较变量以确定哪个更大 */
        blt     r1_lower   /* 如果 r2 更大(N==1),跳转到 r1_lower */
        mov     r0, r1     /* 如果没有跳转,r1 更大(或相等),将 r1 存储到 r0 */
        b       end        /* 继续到结束 */
r1_lower:
        mov r0, r2         /* 到这里说明 r1 小于 r2,将 r2 存储到 r0 */
        b end              /* 继续到结束 */
end:
        bx lr              /* 结束 伪指令 b 用于无条件跳转(branch)到指定的标签*/

这段代码判断哪一个数字更大, 然后将它作为返回值,伪C代码如下:

int main() {
    int max = 0;
    int a = 2;
    int b = 3;
    if (a < b) {
        max = b;
    } else {
        max = a;
    }
    return max;
}

现在使用条件指令和不使用条件指令下的循环:

.global main

main:
        mov     r0, #0     /* setting up initial variable a */
loop:
        cmp     r0, #4     /* checking if a==4 */
        beq     end        /* proceeding to the end if a==4 */
        add     r0, r0, #1 /* increasing a by 1 if the jump to the end did not occur */
        b loop             /* repeating the loop */
end:
        bx lr              /* THE END */

注意

在 ARM 汇编语言中,beq(branch if equal)和 cmp(compare)通常是结合使用的。beq 指令根据零标志(Z)的状态进行跳转。如果零标志被设置(即两个比较的值相等),则执行跳转到指定的标签。

伪C代码:

int main() {
    int a = 0;
    while(a < 4) {
        a += 1
    }
    return a;
}
--------------------
int main() {
    int i = 0;
    for(i = 0; i< 4;i++); 
    return i;
}
7.3 B/BX/BLX

有三种类型的分支指令:

  • Branch(B)

    • 简单的跳转进入一个函数
  • Vramch link (BL)

    • 保存返回地址(PC+4)到LR,然后跳转进函数
      • TIPS:ARM执行一条指令有三个步骤,取址,译码,执行,且当第一条指令取址完成之后,马上开始第二条指令取址,所以,当第一条指令执行的时候,PC已经指向了第三条指令
      • 如果是使用了BL执行了正常的跳转,那么CPU会将返回地址存入LR中,即当前指令地址+4
      • IRQ异常发生:异常发生在指令执行的时候,此时PC指向当前执行指令+8,然后将它存入LR中,所以恢复的时候需要-4以执行下面一条指令
      • 未定义指令异常:发生在译码阶段,此时PC指向当前执行指令+4的位置,返回时不用计算
      • 预取址指令:在执行阶段进入异常,所以PC指向当前执行指令地址+8,返回时需要-4
      • 数据终止异常,在指令执行完毕之后产生,PC指向当前执行指令地址+12,恢复的时候需要-8
  • Branch exchange (BX) and Branch link exchange (BLX)

    • 和B/BL+交换指令集相同( ARM <-> Thumb )
    • 需要用寄存器作为第一操作数:BX/BLX+具体的寄存器

    BX/BLX用来从ARM指令集切换到Thumb指令集

.text
.global _start

_start:
     .code 32         @ ARM mode
     add r2, pc, #1   @ put PC+1 into R2
     bx r2            @ branch + exchange to R2

    .code 16          @ Thumb mode
     mov r0, #1

这里的技巧时获取当前PC的值,然后加1,将它存入一个寄存器,然后 分支(+交换)到这个寄存器。可以看到,加法指令简单的获取有效的PC地址(PC+8)然后加1。接下来,如果我们分支指令后面地址的最低有效位(LSB)是1 的时候,意味着地址不是4字节对齐的,将会发生状态转换。

8 栈和函数

8.1 栈
栈类型存储加载
全降序STMFD(STMDB,递减前存储)LDMFD(LDM,递增后加载)
全升序STMFA(STMIB,递增前存储)LDMFA(LDMDA,递减后加载)
空降序STMED(STMDA,递减后存储)LDMED(LDMIB,递增前加载)
空升序STMEA(STM,递增后存储)LDMEA(LDMDB,递减前加载)
  • 全降序: 栈从高地址向低地址生长。
  • 全升序: 栈从低地址向高地址生长。
  • 空降序: 允许部分空间未使用,栈从高地址向低地址生长。
  • 空升序: 允许部分空间未使用,栈从低地址向高地址生长。
8.2 函数

提示

一个函数可以分为三个部分:

  1. Prologue 序言
  2. Body 函数主体
  3. Epilogue 尾声

这块内容我总结一下就行了。

函数执行时开辟栈空间,是向低内存开辟的,然后基本没啥了,arm就这么结束吧

公告板

持续建设中