资讯详情

ARM汇编基础教程1:寄存器、PC与栈帧调试避坑

📅 2026/11/6 4:21:21 | 华诺云谱 👁 阅读
ARM汇编基础教程1:寄存器、PC与栈帧调试避坑
简介《ARM汇编基础教程1》是一份面向嵌入式开发初学者、ARM平台程序开发者及逆向工程入门者的基础文档围绕ARM汇编语言的核心概念展开帮助读者厘清处理器版本与指令集的对应关系并建立从汇编、链接到可执行程序生成的完整认知。压缩包内仅含1个docx文件大小约2.65MB内容以教程笔记形式组织便于集中阅读和查阅。文档重点讲解有符号与无符号字、半字、字节数据类型大小端字节序与CPSR的E位控制R0—R15通用及特殊功能寄存器的职责以及函数调用中R0—R3传参、SP、LR、PC的协作约定同时附有扩展阅读线索可延伸到指令集、内存读写、条件分支、栈和函数等后续主题。已有211人学习适合作为ARM开发与逆向分析的入门参考。1. 从一条 mov r0, pc 开始ARM 汇编基础教程1 里最容易被忽略的入口第一次把mov r0, pc放进调试器时我以为 R0 会拿到当前指令地址结果它比调试器显示的 PC 多了 8 字节。这个反直觉点把我拉回 ARM 汇编基础教程1也让我意识到 ARM 汇编不是背指令表而是把寄存器、CPSR、ARM/Thumb 状态、栈帧和函数调用串成一条线。新手可以拿这篇当复现路线熟手可以看每个判断背后的依据。下面从最容易被跳过的 PC 和寄存器讲起再把栈帧和避坑点逐个拆开。2. 寄存器与 CPSRARM 汇编基础教程1 先要啃透的 16 个寄存器学 ARM 汇编第一道坎不是指令而是知道数据放哪、状态看哪。原文把数据类型、寄存器、CPSR 放在第二篇这个顺序有道理指令只是动作寄存器和状态位才是动作发生的位置和结果。我一般会先问三个问题——这次操作的数据是字、半字还是字节用的是有符号还是无符号扩展执行完以后 CPSR 的 N/Z/C/V 会不会影响后面的条件跳转。把这三个问题问顺了再看反汇编就不容易把 R0 当万能寄存器、把 PC 当普通变量。2.1 数据类型和字节序先定边界字、半字、字节怎么选ARM 支持对不同数据类型进行操作可以载入和存储的数据类型分成有符号和无符号的字、半字、字节。原文给的后缀很关键半字用-h或-sh字节用-b或-sb字没有扩展。有符号类型可以包含正数和负数所以取值范围较小无符号类型只包含正数和 0取值范围更大。这个差别在读写内存时不是纸面概念读错扩展方式后面拿去做比较或地址计算就会翻车。字节序也要先确认。以 ARMv6 为例指令的固定小字节和数据访问既可以是大字节序也可以是小字节序由程序状态寄存器 CPSR 的第 9 位也就是 E 位控制。实际调试时如果发现内存里读出来的字和预期字节顺序相反先别怀疑指令写错先看 E 位和当前数据访问方式。常见的做法是读内存前先确认这条 load/store 是字节、半字还是字再确认要不要符号扩展最后才看大小端。这个顺序能少走很多弯路。数据类型常见后缀符号与范围典型场景字无扩展有符号/无符号按指令语义处理指针、返回值、32 位整数半字-h/-sh-h无符号-sh有符号16 位短整数、部分硬件寄存器字节-b/-sb-b无符号-sb有符号字符、标志位、协议字段表格里的“后缀”不是让你死记而是提醒自己同样是从内存拿一个值ldrb和ldrsb对最高位的处理不同后续cmp的结果就可能不同。我习惯在反汇编里先把所有 load/store 标出来后缀再去看这个值有没有参与条件判断。如果参与判断就把符号扩展和字节序一起检查一遍这一步比事后追条件跳转便宜得多。2.2 R0-R15 的分工不是背表而是看调用约定寄存器的数量取决于 ARM 版本。根据原文有 30 个 32 位通用寄存器除了 ARMv6-M 和 ARMv7-M 的处理器。基础系列里重点看任意特权模式下都能访问的 R0-R15这 16 个寄存器可以分成通用寄存器和特殊功能寄存器。R0 到 R11 是通用寄存器R12 是 IP 寄存器也叫内部程序调用寄存器R13 是 SP 堆栈指针R14 是 LR 连接寄存器R15 是 PC 程序计数器CPSR 是当前程序状态寄存器。真正有用的记法不是背表而是把寄存器和调用约定绑在一起。ARM 专用的函数调用规则规定函数的前四个参数分别存放在 R0 到 R3 中函数结果通过 R0 返回如果返回值是 64 位就用 R0 和 R1 组合。R11 在系统调用发生时存储系统调用数值同时作为栈帧指针帮助追踪栈帧边界。R13 指向栈顶想获得 32 位数据空间就让栈指针减去 4。R14 在函数调用时保存返回地址让子函数执行完能回到父函数继续走。寄存器别名/角色我通常先看什么R0-R3参数/返回值/临时数据函数开头有没有被赋值调用后 R0 是不是返回值R4-R11通用数据、局部变量、栈帧指针R11 是否被用作 FP函数里有没有保存它R12IP内部程序调用寄存器调用跳转前后的临时地址R13SP堆栈指针每次 push/pop 后是否按 4 字节变化R14LR连接寄存器函数调用后返回地址有没有被覆盖R15PC程序计数器当前指令地址和调试器显示差多少CPSR当前程序状态寄存器N/Z/C/V 和 T 位、E 位这张表我一般放在手边但不会把它当答案。拿到一份反汇编先看函数开头有没有push {r11, lr}再看 R0-R3 在调用前怎么赋值最后看 R13 在函数主体里减了多少。这样能快速判断这是叶函数还是非叶函数也能看出参数是寄存器传的还是栈上传的。原文提到超过四个参数时要用堆栈存储剩余参数返回值在 R064 位结果用 R0 与 R1 组合。这些规则比单独记“R0 是累加器”更抗忘。2.3 PC 为什么总比调试器显示的地址多 8 字节CPSR 标志位怎么读PC 是最容易让人怀疑调试器的寄存器。原文的示例里mov r0, pc执行时调试器显示 PC 是0xaaad43d8执行后 R0 却是0xaaad43e0正好多 8 字节。解释有两层一层是 ARM 保留早期处理器兼容性老款 ARM 处理器总是获取当前已执行指令的后两条指令地址另一层是 CPU 流水线取指令、解码指令和执行指令使用不同硬件部件可以并行执行。RISC CPU 指令长度一定CPU 在解码当前指令时就能取下一条指令在执行当前指令时对下一条指令解码并取下下一条形成三级流水线。调试器为了展示更有逻辑性显示的是当前执行指令地址真实读 PC 时要按架构规则看。.text .global main main: mov r0, pc 把 PC 附近地址读进 R0ARM 状态下常见为当前指令 8 mov r1, #2 随便放一条后续指令方便观察流水线影响 add r2, r1, r1 R2 4 bx lr 返回这段代码的逻辑不是让你依赖固定地址而是让你在调试器里观察 PC 的读取规则。参数说明mov r0, pc的目标是 R0源是 PCmov r1, #2设置一个立即数add r2, r1, r1做加法bx lr用 LR 返回。实际运行时地址每次可能不同不要拿示例地址当常量。要看懂结果先确认当前是 ARM 状态还是 Thumb 状态ARM 状态下 PC 常见为当前指令加 8 字节Thumb 状态下常见为当前指令加 4 字节。调试器显示值和指令真实读到的值不是一回事。CPSR 的标志位也要按“谁改的、谁用的”来看。N 是 Negative指令执行结果为负时置 1Z 是 Zero结果为 0 时置 1C 是 Carry加法有进位则置 1减法有借位则置 0、否则置 1V 是 overflow结果超出 32 位补码存储范围时置 1E 是 Endian-bit置 0 用小端序置 1 用大端序T 是 Thumb-bit置 1 用 Thumb 模式置 0 用 ARM 模式M 是 Mode-bit共 5 位表示处理器运行模式。N、Z、C、V 分别和 x86 的 SF、ZF、CF、OF 对应但别把寄存器名也照搬。.text .global main main: mov r1, #1 mov r2, #2 cmp r1, r2 1 - 2 -1结果为负N 置 1 cmp r2, r1 2 - 1 不借位C 置 1 cmp r2, r2 2 - 2 0Z 置 1同时 C 也置 1 bx lr这段代码的重点是cmp只改 CPSR不改参与比较的寄存器。参数说明cmp r1, r2做隐式减法r1和r2的值不变后面的条件跳转要根据 N/Z/C/V 组合判断。判断依据是“比较结果落在哪个标志位”不是“哪个寄存器被改了”。如果cmp后面又插了一条会改标志位的指令条件跳转就会按新的标志位走这是后面避坑章要展开的典型翻车点。 注意示例地址和寄存器值只用于观察规则实际调试以当前程序和 CPSR 状态为准。3. ARM 与 Thumb 指令集写偏移量小的代码前必须分清的状态切换ARM 处理器有两种工作状态ARM 和 Thumb。这两种状态和运行模式没有关系ARM 状态和 Thumb 状态的代码都可以运行在用户模式下。最大的差异是指令集ARM 状态的指令长度是 32 位Thumb 状态的指令长度是 16 位也可能为 32 位。原文特别提醒了解如何使用 Thumb 工作状态对编写 ARM 平台的漏洞利用至关重要写 ARM shellcode 时常用 16 位 Thumb 指令代替 32 位 ARM 指令避免指令中出现\0截断。这里不展开攻击 payload只把状态切换和指令选择讲清楚因为这是读懂后续内存读写和条件分支的前提。3.1 ARM 与 Thumb 的指令长度和条件执行差异ARM 状态下的所有指令都支持条件执行这是它和早期 Thumb 的一个重要差别。Thumb 的不同版本支持情况不一样Thumb-1 是 16 位指令用于 ARMv6 和更早版本Thumb-2 是对 Thumb-1 的扩展添加了更多指令并允许它们为 16 位或 32 位宽出现在 ARMv6T2 和 ARMv7ThumbEE 在 Thumb-2 基础上包含针对动态代码生成的一些变更和补充。某些 ARM 处理器版本允许使用it指令在 Thumb 中进行条件执行但后来的版本又删除了这个指令。我的判断依据很简单先确认目标设备版本和该版本对 Thumb 的支持再决定代码里能不能依赖条件执行和 32 位 Thumb 指令不凭印象写。32 位 Thumb 指令有.w后缀这个后缀在反汇编里很显眼。看到.w就要意识到这条指令占 32 位不是所有 Thumb 指令都只有 16 位。条件执行在 ARM 状态下更自然在 Thumb 状态下要看版本和it指令支持。常见的做法是如果目标环境是较老的 Thumb-1条件执行尽量用条件分支显式写如果目标环境支持 Thumb-2再考虑用更紧凑的指令组合。这个选择不是“哪个高级用哪个”而是“目标设备支持哪个、调试器能不能稳定复现”。3.2 BX/BLX 切换状态最低有效位不是对齐错误要切换处理器执行状态可以使用分支指令BX或BLX并把目标寄存器的最低有效位置 1。原文举的例子是把 1 加到偏移量上比如0x55301。初看会担心对齐问题因为指令总是 2 或 4 字节对齐但处理器在读取指令时会忽略最低有效位所以这个 1 不是地址的一部分而是状态提示。当前程序状态寄存器里的 T 位置 1就说明处于 Thumb 模式。判断依据是“目标地址最低位 T 位 目标代码实际编码”不是只看地址能不能被 2 整除。.text .global main main: ldr r0, thumb_entry1 目标地址最低位置 1提示切换到 Thumb bx r0 分支并切换状态 后续 ARM 状态指令省略 thumb_entry: mov r1, #1 Thumb 入口示例指令 bx lr这段代码的逻辑是演示状态切换的地址写法。参数说明ldr r0, thumb_entry1把符号地址加 1 放进 R0具体伪指令写法依汇编器而定bx r0用 R0 做分支并更新状态thumb_entry是目标标签。实际项目里不要手写死地址应该让汇编器或链接器处理符号。调试时如果bx后跑飞先看目标地址最低位有没有置 1再看 T 位和反汇编窗口是不是已经切到 Thumb。这个“最低位”不是玄学是状态切换约定里最容易被忽略的一个 bit。3.3 桶形移位器和可变操作数一条指令省出几条ARM 指令通常跟一到两个操作数模板可以理解为助记符、S 扩展位、执行条件、目的寄存器、第一个操作数、第二个操作数。第二个操作数叫可变操作数因为它可以是立即数、寄存器也可以是带移位操作的寄存器。原文列了几种形式#123是立即数Rx是寄存器Rx, ASR n是算术右移 n 位Rx, LSL n是逻辑左移 n 位Rx, LSR n是逻辑右移 n 位Rx, ROR n是循环右移 n 位Rx, RRX是带扩展的循环右移 1 位。桶形移位器是 ARM 模式特有的功能它能把移位包含在指令里减少指令数量。.text .global main main: mov r0, #3 mov r1, r0, lsl #1 R1 R0 1相当于 R0 * 2 add r2, r1, r0, lsr #1 R2 R1 (R0 1) bx lr这段代码展示可变操作数的写法。参数说明mov r1, r0, lsl #1的源操作数是r0左移 1 位结果放 R1add r2, r1, r0, lsr #1的第二个操作数是r0逻辑右移 1 位再和 R1 相加。判断依据是如果一条指令里已经带了移位就不要再多写一条单独的移位指令但前提是目标指令集支持这种寻址形式。Thumb 状态下不是所有形式都可用反汇编里看到.w或指令被拆开就要回头检查版本支持。省指令不是目的少出状态依赖才是目的。4. 栈与函数栈帧从 push/pop 到 prologue/epilogue 的完整复现栈是 ARM 汇编里最容易“看会了、写就错”的部分。原文说栈用来存储临时数据使用push、pop指令和栈交互但push和pop是一系列其他内存相关指令的别名不是实际指令。一条 32 位数据将被放置的实际地址由栈指针定义地址存在 SP 寄存器里。SP 可能指向栈中当前最新的项也可能指向当前项的下一可用内存槽前者是 full 实现后者是 empty 实现。再加上 descending 和 ascending就出现 full descending、full ascending、empty descending、empty ascending 几种组合。判断依据是先确认当前栈实现再看push后 SP 是加还是减不能只看指令名。4.1 full descending 栈的 sp 变化逐条跟踪原文给了一个 full descending 栈的例子执行mov r0, #2后SP 地址是0xfffefac0R0 是 2执行push {r0}后SP 地址变成0xfffefac0 - 4 0xfffefabcR0 还是 2执行mov r0, #3后SP 不变R0 变成 3执行pop {r0}后SP 从0xfffefabc读 4 字节数据然后 SP 变成0xfffefabc 4 0xfffefac0R0 恢复为 2。这个例子把 full descending 的“先减后存、先取后加”讲得很直白。.text .global main main: mov r0, #2 R0 2 push {r0} SP SP - 4把 R0 写到新 SP 指向的位置 mov r0, #3 只改 R0不动栈 pop {r0} 从 SP 读 4 字节到 R0然后 SP SP 4 bx lr这段代码的逻辑是观察 SP 和 R0 的分离。参数说明push {r0}的寄存器列表里只有 R0实际写入宽度按架构规则pop {r0}的目标也是 R0。判断依据是push之后 SP 变小说明是 descendingSP 指向最后一个已用项说明是 full。实际调试时不要背示例地址0xfffefac0每次运行栈地址都可能不同要看的是相对变化 4 字节。执行动作SP 变化R0 值观察点mov r0, #2不变2栈还没动push {r0}减 42数据写入新 SP 位置mov r0, #3不变3只改寄存器不改栈pop {r0}加 42从栈恢复旧值这张表适合在调试器里逐条单步对照。我一般会在push前后各看一次sp和x/4wx $sp确认写入位置和值在pop前后再看一次确认 SP 回到原值。如果 SP 变化不是 4 的倍数先检查寄存器列表和栈实现再检查是不是混用了不同位宽的 load/store。栈的问题很少是“指令不认识”多数是“假设错了实现方式”。4.2 prologue 和 epilogue叶函数与非叶函数的差别函数利用栈保存局部变量和寄存器状态。为了让所有事物有序运行函数使用栈帧也就是栈中一片本地化内存区域专用于特定函数。栈帧在函数的 prologue 中创建把栈帧指针 FP 设置到栈帧底部然后为栈帧分配的栈缓存被开辟。栈帧从底部开始通常包含返回地址 LR、先前的帧指针、需要保存的寄存器、函数参数如果函数接受超过 4 个参数、局部变量等。实际存储内容可能不同但这些是最常见的。函数运行到结尾时栈帧在 epilogue 中被破坏。ARM 函数结构可以拆成三部分prologue 序言、body 函数主体、epilogue 尾声。prologue 的目的是保持抽象的先前状态通过把 LR 和 R11 的值存储到栈上并为局部变量开辟栈空间。原文示例是push {r11, lr}开始add r11, sp, #0设置栈帧底部sub sp, sp, #16分配缓存区。body 负责执行任务可以分支到其他函数示例里用mov r0, #1、mov r1, #2、bl max设置参数并调用。epilogue 把程序恢复到调用前状态示例是sub sp, r11, #0调整 SPpop {r11, pc}恢复 FP 并把保存的 LR 载入 PC。.text .global main main: push {r11, lr} prologue保存帧指针和返回地址 add r11, sp, #0 设置当前栈帧底部 sub sp, sp, #16 为局部变量开辟 16 字节空间 mov r0, #1 第一个参数 mov r1, #2 第二个参数 bl max 调用子函数返回地址写入 LR sub sp, r11, #0 epilogue恢复 SP 到栈帧底部 pop {r11, pc} 恢复 FP并把保存的 LR 载入 PC这段代码的逻辑是标准非叶函数骨架。参数说明push {r11, lr}保存两个寄存器SP 通常减 8add r11, sp, #0让 R11 指向当前栈帧底部sub sp, sp, #16给局部变量留空间bl max会更新 LR所以前面必须保存 LRpop {r11, pc}同时恢复帧指针和返回地址。判断依据是函数里只要还有bl或blx调用别的函数它就是非叶函数LR 会被覆盖prologue 里就要保存 LR。叶函数本身不调用/分支其他函数结构可以更简单但一旦后面加了调用就要按非叶函数补保存。4.3 用调试器观察栈帧时该看哪几个寄存器观察栈帧不要一上来就看全部寄存器先看 SP、R11、LR、PC 和 CPSR。SP 告诉你栈顶在哪R11 告诉你当前栈帧底部在哪LR 告诉你返回地址有没有被保存PC 告诉你当前执行到哪CPSR 的 T 位告诉你当前是 ARM 还是 Thumb。原文提到在调试器里可以看到栈的视图R11 FP 是栈帧寄存器。我的习惯是先在 prologue 后停一次记录 SP 和 R11在 body 里每次bl前后各看一次 LR在 epilogue 前看 R11 是否还指向当初设置的栈帧底部。gdb -q ./program # 进入调试器后 info registers sp r11 lr pc cpsr x/8wx $sp这段命令的逻辑是先看关键寄存器和栈内存。参数说明info registers后跟寄存器名可以只看关心的项x/8wx $sp以 16 进制字为单位查看 SP 开始的 8 个 word。判断依据是如果pop {r11, pc}后程序跳飞先看栈顶保存的 LR 是不是调用后的下一条指令地址如果 R11 在 body 里被改掉epilogue 的sub sp, r11, #0就会把 SP 带到错误位置。栈帧问题不要靠猜按“SP 从哪来、R11 指向哪、LR 存了什么”三件事查基本能定位。5. 常见问题与避坑ARM 汇编入门最容易翻车的五个点ARM 汇编入门阶段的错误大多不是指令拼错而是假设错了环境。有人把 PC 当普通寄存器算地址有人把 Thumb 目标地址直接写过去有人cmp后面插了一条改标志位的指令还怪条件跳转不生效。下面五个坑是我见过频率最高的每个都按“现象、原因、解决”拆开。你不需要全部背下来但调试时遇到相似现象可以按这个顺序排查比盲目改指令省时间。5.1 链接时报 main 未定义先看入口符号和链接方式现象汇编文件单独as能过ld链接时报undefined reference to main或者程序入口跑不到自己写的第一条指令。原因常见情况是汇编文件里没有把入口符号声明为全局或者链接时默认入口和你写的标签不一致也有人把_start和main混用却没有对应的启动代码。原文的汇编链接流程是as program.s -o program.o再ld program.o -o program这个流程依赖符号和入口设置正确。解决先检查文件里有没有.global main或对应的全局入口声明再看链接命令有没有指定入口。如果写的是独立入口就不要假设有 C 运行时帮你调main如果写的是main就确认链接方式能接受这个符号。最稳的做法是先保留最小可执行骨架链接通过后再加指令别一上来就堆函数调用。5.2 PC 值对不上单步结果和预期差 8现象调试器显示 PC 是某个地址执行mov r0, pc后 R0 却多了 8 字节换到 Thumb 状态又变成多 4 字节让人怀疑调试器坏了。原因ARM 的 PC 读取受流水线影响调试器显示的是当前执行指令地址真实读 PC 时按架构规则可能已经指向后面两条指令。原文用三级流水线解释了这个差异并提醒调试器显示经过了处理。解决先看 CPSR 的 T 位确认状态ARM 状态按当前指令加 8 字节理解Thumb 状态按加 4 字节理解写位置无关代码时不要假设mov r0, pc一定等于当前指令地址。需要取当前地址时用标签和相对偏移让汇编器/链接器算不要手写固定偏移。调试时把“调试器显示值”和“指令真实读到的值”分开记录能少很多误判。5.3 push/pop 后 sp 不是 4 字节对齐现象push一个寄存器后 SP 只变了 2或者pop后 SP 回不到原值接着访问内存就异常。原因常见原因是混用了不同位宽的栈操作或者对当前栈实现假设错误full/empty 和 descending/ascending 组合不同SP 是先减还是后减、指向已用项还是空槽都不一样。原文特别强调push/pop是别名实际地址由 SP 定义。解决先确认当前栈是 full descending 还是其他实现再用单步看push前后 SP 的相对变化。写函数骨架时统一用 4 字节对齐的寄存器列表别在同一个函数里混着手写str sp, [sp, #-4]和push。如果 SP 初值本身没对齐先修初始化不要在函数里硬补偏移。5.4 Thumb 代码跳转后跑飞现象bx到目标标签后取指异常或者反汇编窗口显示的指令和源码对不上。原因切换到 Thumb 时目标寄存器最低有效位要置 1处理器读取指令时会忽略最低位如果没置 1可能仍按 ARM 状态取指或者地址计算错。原文明确说可以用BX或BLX把目标寄存器最低有效位置 1T 位置 1 表示 Thumb 模式。解决检查目标地址是不是符号1检查跳转后 CPSR 的 T 位检查目标标签处是不是真的按 Thumb 编码。不要用“地址能被 2 整除”判断状态状态看最低位和 T 位。调试时在bx前后各看一次目标寄存器和 CPSR跑飞原因通常一眼就能看到。5.5 cmp 后条件跳转不生效现象明明写了cmp r1, r2后面的bne却不跳或者跳转方向和预期相反。原因cmp只改 CPSR不改参与比较的寄存器如果cmp和条件跳转之间插入了会改标志位的指令条件跳转就会按新标志位走。另一种原因是条件码选错把有符号比较和无符号比较混用。原文用 1 和 2、2 和 1、2 和 2 的例子说明 N、Z、C 的变化。解决让cmp紧挨条件跳转中间不要插movs、adds这类带 S 后缀的指令先确认要判断的是相等、负数、进位还是溢出再选条件码。调试时看 CPSR 的 N/Z/C/V别只看寄存器值。如果条件跳转还是不生效把cmp前后的标志位截图对比通常能发现是哪条指令偷偷改了状态。6. 把基础教程跑成自己的验证清单从汇编到可执行文件的最小闭环这份 ARM 汇编基础教程1 按七篇展开从 ARM 汇编简介、数据类型和寄存器、指令集、内存读写、高级内存读写、条件分支到栈和函数适合当手边索引而不是一次性读物。我的用法是每学一个概念就落成一个最小闭环写一段带注释的.s用as汇编用ld链接再用调试器看寄存器、CPSR 和栈。闭环跑通一次比本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑