资讯动态

ARM汇编入门:R0-R15寄存器、CPSR与最小程序实战

发布时间:2026/10/26 18:55:18 来源:尧图企业网站定制
简介这份 ARM 汇编基础教程面向嵌入式开发与逆向工程入门者从零梳理 ARM 汇编的核心概念帮助读者建立处理器版本、指令集与寄存器体系的整体认知。教程依次讲解 ARM 处理器版本与指令集的映射关系、.s 源文件经 as 汇编与 ld 链接生成可执行程序的完整流程、有符号与无符号的字/半字/字节数据类型以及由 CPSR 的 E 位控制的大小端字节序随后逐一说明 R0–R15 与 CPSR 的职责包括 R0 作累加器与返回值、R11 作栈帧指针、R13 栈指针、R14 保存返回地址、R15 程序计数器并结合三级流水线解释读取 PC 时出现地址偏移的原因末尾补充前四个参数经 R0–R3 传递的调用约定与延伸阅读书目。压缩包共 1 个 docx 文档约 2.65MB按篇组织、便于对照查阅。已有 209 人学习可作为 ARM 平台开发与漏洞分析入门的起点也为内存读写、条件分支、栈和函数等后续篇目做好铺垫。1. 新手第一眼ARM 汇编基础教程1 到底是个啥你第一次打开调试器里的反汇编窗口是什么感觉我猜和我差不多满屏的 mov、push、pop、bx像在看一门没人教过的外语。其实它们没那么玄说白了就是 CPU 一条一条真正在执行的“指令条”。ARM 汇编基础教程1讲的就是这些指令条长什么样、怎么读、怎么写以及一个后缀为 .s 的文本文件怎么被 as 和 ld 一步步加工成能跑起来的程序。它属于 ARM 汇编入门的第一站内容覆盖处理器版本与指令集的对应关系、数据类型、R0 到 R15 寄存器、CPSR 状态位以及一点栈和函数调用的影子。适合刚接触底层、想搞懂程序在真机上到底干了什么事的新手。读完这一篇你至少能对着一条 mov 指令说出它在动哪个寄存器不再被调试器吓退。2. 为什么要学 ARM 汇编三个场景让你觉得它真用得上很多新手听到“汇编”两个字就头大心里想的是我平时写 Python、写 Java连指针都不太碰学这个干嘛。这个反应很正常我一开始也这么想。但有几类时刻汇编会突然从“选修课”变成“必修课”你躲都躲不掉。下面这三个场景是我自己以及身边人真实遇到过的不是吓唬人。2.1 场景一程序在真机上崩了C 代码什么线索都没给你写了一段 C 程序在自己的电脑上跑得好好的一放到目标设备上就挂了。设备只返回一个崩溃地址加一段栈回溯行号、函数名全是问号。你打开源码左看右看找不到问题在哪。这时候唯一的线索就是那段地址对应的反汇编。你得能看懂ldr r3, [r0, #4]是在从一个结构体里取第四个字节之后的字段才能反推出是哪个指针为空、哪次访问越界。看不懂汇编这条线索就等于没有你只能靠猜或者靠加一堆 printf 去试这在嵌入式环境里往往连 printf 都没有。这个场景不是只有做底层的人才会遇到。做后端性能分析、做崩溃日志归类、做安全审计只要涉及“没有源码或源码不可信”的现场汇编就是唯一能用的语言。学它的目的不是让你天天手写汇编而是让你在关键时刻有工具可查。提示新手最容易犯的错是“等遇到了再学”。真到线上崩溃那一刻你连寄存器哪个是参数、哪个是返回值都分不清根本来不及。2.2 场景二看逆向分析、安全研究时ARM 是绕不开的语言手机、路由器、各种带算力的小设备绝大多数跑在 ARM 架构上。你做协议分析、做固件检查、做恶意样本的静态阅读拿到的往往是一坨二进制没有符号表没有源码。你要判断这段代码在干什么只能从入口点开始一条指令一条指令地读。读得懂的人十分钟能画出调用关系读不懂的人盯着十六进制发呆。ARM 汇编基础教程1 这种入门材料价值就在于它先把“指令集长什么样、寄存器怎么用”这层窗户纸捅破让你后面读真实样本时不至于从零开始。这里我要提醒一句别一上来就去啃厚厚的架构手册。新手最容易被那种几百页的文档劝退。先跟着教程把常用指令认熟把 R0-R15 的职责记牢比背完整本手册有用得多。2.3 场景三嵌入式与底层调优每一条指令都算数在资源紧张的设备上代码大小和周期都是硬指标。同一段逻辑用不同的写法编译出来可能差几十个字节也可能差几十个时钟周期。你如果只看 C 代码是感受不到这些差异的只有看反汇编才能发现编译器把你的循环展开成了什么样、有没有偷偷插入栈保护、某个判断是不是被优化成了条件执行。知道mov和ldr在字节数上的差别你才知道为什么固件会超容量为什么某个中断响应慢了一拍。这类知识在纯高级语言的世界里是学不到的。说了这么多“为什么”接下来该动手了。新手最先要做的不是写代码而是把工具装好。如果你本来就在一块 ARM 开发板或者树莓派上工作系统里通常已经带了 binutils直接用就行如果你在普通的 x86 电脑上需要一套交叉工具链再加一个能在本机模拟运行 ARM 程序的工具。# 在 Debian/Ubuntu 系主机上安装交叉汇编、链接、调试和模拟运行工具 sudo apt update sudo apt install -y gcc-arm-linux-gnueabihf qemu-user gdb-multiarch这三样东西各管一段gcc-arm-linux-gnueabihf这个包里带着arm-linux-gnueabihf-as和arm-linux-gnueabihf-ld负责把 .s 变成目标文件和可执行文件qemu-user让你在 x86 机器上直接跑编译出来的 ARM Linux 程序不需要真的买一块板子gdb-multiarch是能调试多种架构的 GDB后面看寄存器全靠它。装完之后可以用arm-linux-gnueabihf-as --version验证一下能打印出版本信息就说明装好了。3. 第一个例子怎么跑起来从 .s 文件到可执行程序的全过程工具装好之后最怕的就是“不知道从哪下手”。我的建议是第一个例子越小越好小到只剩三条指令跑通之后再往上加。这一章就带你走一遍完整流程包括写文件、汇编、链接、运行以及每一步到底发生了什么。别急着背指令先让程序跑起来成就感比什么都重要。3.1 写一个最小的 .s 文件并弄懂每一行新建一个文件命名成first.s注意后缀是小写的.s大写.S会被 C 预处理器先处理一遍新手很容易在这里翻车。文件内容如下 first.s最小的 ARM Linux 可执行程序 .text 告诉汇编器下面是代码段 .global _start 把 _start 导出为全局符号链接器靠它找入口 _start: mov r0, #42 r0 放退出码42 这个数字随便换 mov r7, #1 r7 放系统调用号1 在 ARM Linux 上代表 exit svc #0 触发软中断把控制权交给内核程序结束逐行解释一下。.text是段声明表示后面的内容放进代码段不写它汇编器也能过但产物结构会乱新手最好养成习惯。.global _start把_start这个标签暴露给链接器链接器默认就是找这个名字当程序入口名字写错会报 “cannot find entry symbol”。mov r0, #42是把立即数 42 放进 r0在 Linux 的系统调用约定里r0 既是第一个参数也是返回值。mov r7, #1把系统调用号 1 放进 r71 就是 exit。最后svc #0是软中断指令CPU 从这里跳进内核内核看到 r7 是 1就按退出处理把 r0 的值当作退出码返回给 shell。为什么不用bx lr结尾因为_start是程序入口没有“调用者”给你返回lr 里装的值不可靠直接 bx lr 基本就是段错误。新手第一个例子跑出 Segmentation fault十有八九是这里用错了。用 exit 系统调用收尾是最稳的写法。3.2 汇编、链接两步走as 和 ld 各自做了什么接下来执行两条命令。注意如果你用的是交叉工具链命令名前面要带前缀下面用arm-linux-gnueabihf-举例# 第一步汇编把 .s 文本翻译成 .o 目标文件 arm-linux-gnueabihf-as -o first.o first.s # 第二步链接把 .o 组织成可执行文件 arm-linux-gnueabihf-ld -o first first.o第一条命令调的是汇编器 as它做的事很单纯把每条汇编指令翻译成对应的机器码输出一个.o文件。这个阶段还不会解析外部符号也不会决定代码最终放在内存的哪个地址。第二条命令调的是链接器 ld它负责把.o里各个段拼起来决定入口地址处理符号引用最后产出可执行文件first。有新手会问我平时都直接gcc hello.c -o hello为什么这里要拆成两步原因很实际。gcc 一步到位会顺手塞进启动代码、C 库、一堆你没见过的段新手看不到中间产物出了问题也不知道是哪一步坏的。拆成 as 和 ld你能清楚地看到每一步产出了什么first.o和first各是什么哪个阶段报错就查哪一步。这种“看得见每一步”的感觉对入门特别重要。3.3 在 x86 主机上跑起来qemu 的用法和退出码检查可执行文件有了但你的主机是 x86直接./first会报“无法执行二进制文件”。这时候 qemu-user 就派上用场了# 用 qemu 模拟运行 ARM 程序注意路径要写对 qemu-arm ./first # 紧接着打印上一条命令的退出码 echo $?正常的话第二条命令会输出 42。这个 42 就是你写在 r0 里的值经过 exit 系统调用传给了 shell。看到这个数字说明整条链路都通了汇编没报错、链接找到了入口、程序按预期退出。这是新手最该先拿下的一个里程碑。如果你手上正好有 ARM 设备或者本来就在 ARM 的 Linux 环境里可以跳过 qemu直接./first效果一样。工具链前缀也去掉用系统自带的as和ld即可。注意echo $?必须在运行程序的下一条命令里执行中间插了别的命令拿到的就是别的命令的退出码。这个小细节坑过不少人。把这三步走通你对“汇编程序是怎么来的”就有了体感。后面所有复杂的例子都是在这个最小骨架上加寄存器、加内存访问、加分支。骨架稳了加东西才不会慌。4. 数据类型和寄存器R0 到 R15 与 CPSR 到底装了什么第一个例子跑通之后接下来要补的是“词汇量”。汇编里翻来覆去就那几样东西数据有哪几种宽度、放在哪里寄存器有几个、各管什么状态位怎么表示上一次运算的结果。这一章把这几件事讲清楚后面读指令才不会一头雾水。4.1 数据宽度与符号扩展-h、-sh、-b、-sb 到底差在哪ARM 支持对几种宽度的数据进行载入和存储按大小分成字、半字、字节。字是 32 位半字是 16 位字节是 8 位。指令里用后缀来区分宽度半字用-h字节用-b字不加后缀。有符号和无符号的区别在于有符号能表示负数取值范围小一些无符号只表示 0 和正数取值范围大一些。带符号的版本在后缀前面加s也就是-sh和-sb。这几个后缀不是装饰它们决定了数据从内存读进寄存器时怎么扩展。举个例子内存里有一个字节0xFF。用无符号方式读寄存器里得到0x000000FF也就是 255用有符号方式读最高位被当成符号位得到0xFFFFFFFF也就是 -1。同一个字节读法不同结果差了十万八千里。新手写解析代码时如果漏了这个s很容易出现“明明内存里是 255怎么算出来是 -1”的困惑。另一个跟数据有关的概念是字节序。ARM 处理器可以工作在小端序也可以工作在大端序具体由程序状态寄存器 CPSR 里的 E 位控制E 位为 0 用小端为 1 用大端。小端的意思是低位字节放在低地址大端反过来。新手不用一上来就死记只要记住“同一个 32 位数在不同字节序下内存里看到的字节顺序是反的”调试时用 gdb 看内存就能对上。4.2 通用寄存器 R0-R12临时工、参数和返回值ARM 的寄存器数量跟具体版本有关入门阶段只需要盯住 R0 到 R15 这 16 个可以直接访问的寄存器就够。其中 R0 到 R11 是通用寄存器R12 是内部程序调用寄存器R13 是栈指针 SPR14 是连接寄存器 LRR15 是程序计数器 PC。另外还有一个 CPSR记录当前程序状态。用一张表把它们摆清楚寄存器别名主要用途R0-R3参数/返回值函数调用的前四个参数返回值默认放 R0R4-R10通用存局部变量、指针、临时数据R11FP栈帧指针帮忙定位当前函数的栈边界R12IP内部程序调用用的临时寄存器R13SP栈指针指向栈顶R14LR连接寄存器函数调用时保存返回地址R15PC程序计数器指向下一条要执行的指令对新手来说最需要先形成肌肉记忆的是两条规则第一函数的前四个参数依次放在 R0 到 R3第二函数的返回值默认放在 R0。这两条规则在后面读任何函数调用时都会反复出现。R0 还经常被当作累加器用算数运算的中间结果放它那里很顺手。R11 在系统调用场景下会存调用号同时它作为栈帧指针帮你在栈上划出当前函数的地盘后面讲栈的时候还会遇到它。4.3 SP、LR、PC 和 CPSR进程状态的三块仪表盘SP 指向栈顶。栈是内存里划出来的一块区域用来存临时数据、保存返回地址、传递超过四个的参数。往栈上要空间的方式很直接想让 SP 往下挪 4 个字节就减 4。SP 一挪栈就“长”出来一块。LR 在函数调用时被更新成返回地址——也就是调用指令的下一条指令的地址。被调用的函数执行完之后把 LR 里的值装回 PC程序就回到调用点继续往下走。这个机制是函数能“去了还能回来”的基础。PC 是最容易让新手困惑的一个。ARM 状态下一条指令 4 个字节Thumb 状态下 2 个字节。当一条分支指令执行时PC 保存目标地址平时读 PC 的时候拿到的值往往比当前正在执行的指令地址大 8。为什么是 8一种解释是历史兼容老款 ARM 处理器取指时就是超前两条指令另一种解释是三级流水线——取指、解码、执行三个硬件部件并行工作执行当前指令时PC 已经在取下下一条指令了。调试器为了让你看着顺眼通常显示的是“当前指令地址”跟硬件里真实的 PC 值差一截。新手在 gdb 里单步时如果发现 PC 对不上先别怀疑自己算错了多半是这个显示差异。CPSR 里最值得先认识的是 N、Z、C、V 四个标志位它们分别对应“结果为负”“结果为零”“有进位或借位”“溢出”。这四位不是给你直接读的而是给条件执行和循环指令用的。看个例子 flags.s比较 1 和 2观察 CPSR 标志位变化 .text .global _start _start: mov r1, #1 r1 1 mov r2, #2 r2 2 cmp r1, r2 做一次隐式减法1 - 2 -1 mov r0, #0 退出码 0 mov r7, #1 exit 系统调用号 svc #0cmp r1, r2不改变 r1 和 r2 的值只根据减法结果去改 CPSR。1 减 2 等于 -1是负数所以 N 位被置 1。如果你把两条 mov 的顺序换一下再比较结果为正C 位会置 1。如果比较的是两个相等的数减法结果为 0Z 位被置 1。想看实际效果用 gdb 在 cmp 后面下个断点执行info registers和p/x $cpsr就能看到标志位。# 启动调试器加载刚才编译出来的程序 gdb-multiarch ./flags # 在 gdb 里依次执行 # (gdb) break _start # (gdb) run # (gdb) stepi # 单步执行一条指令连按几次走到 cmp 之后 # (gdb) info registers # (gdb) p/x $cpsr # 看 CPSR 的十六进制值对照 N/Z/C/V 位把这几步走一遍你对“寄存器是活的”这件事就有感觉了。后面读条件分支、读循环看到某条指令“有条件地执行”你就知道它是在查 CPSR 的哪一位。5. 新手最常见的 5 个坑现象、原因、排查一遍入门阶段踩的坑翻来覆去就那么几个。我把它们集中放在这一章按“现象—原因—解决”写清楚。你遇到报错时先来这里对一遍比漫无目的地搜要快得多。5.1 汇编和链接阶段就翻车两条最常见的报错坑一汇编器报 “Error: bad instruction”。现象是你照着教程敲的指令as 死活不认。原因通常有三种文件后缀写成了大写的.S被 C 预处理器先扫了一遍把注释或者某些符号吃掉了指令拼写错了一个字母比如把mov写成mvo或者你用的架构版本不支持这条指令。解决办法是先确认文件后缀是小写.s再对着指令表逐个字母核一遍最后确认工具链的架构参数匹配目标设备。坑二链接器报 “warning: cannot find entry symbol _start”。现象是 as 通过了ld 给出这个警告产出的程序跑起来行为诡异。原因是链接器默认找_start当入口你要么忘了写.global _start要么标签名拼成了start或者_main。解决方法是补上全局声明或者在链接时用-e参数显式指定入口名字。我自己的习惯是每个 .s 文件开头都先把.global _start写好再填内容这样不容易漏。5.2 跑起来才翻车崩溃、PC 对不上、栈方向搞反坑三程序一跑就 Segmentation fault代码看起来没问题。现象是 qemu 或者真机直接崩溃用 gdb 看停在一条莫名的指令上。最常见的起因是用bx lr结尾——_start是入口没有调用者LR 里的值不可控跳过去就是崩溃。解决办法是把结尾改成 exit 系统调用或者一开始就用 C 库的启动流程。这类崩溃还有一个变种忘了给栈分配空间就 pushSP 指向非法地址照样崩。坑四在 gdb 里读 PC算出来的地址跟预期差 8。现象是你明明在执行地址 A 的指令读 PC 却读到 A8怀疑自己算错了。原因是 ARM 的流水线设计和历史兼容约定PC 在硬件上确实超前调试器显示的是处理过的值。解决办法是记住这条规则在 ARM 状态下读 PC默认按“当前指令地址 8”理解在 Thumb 状态下按“ 4”理解。写位置无关代码或者算偏移时把这条规则考虑进去就不会对不上。坑五push 之后 SP 变小了搞不清栈是往上长还是往下长。现象是你以为 push 会让 SP 变大结果 gdb 里看到 SP 减了 4。原因是 ARM 常用的是“满降序”栈栈向低地址方向生长SP 指向栈中最后一个已用项。push 的动作是先减 SP再存数据pop 是先读数据再加 SP。解决办法是记住“满降序”这三个字写代码时先想清楚当前 SP 指向哪里再决定减多少。拿一张纸画一下内存地址的变化比盯着屏幕硬想快得多。提示这五个坑里前两个在汇编链接阶段就能发现后三个要跑起来或者调试时才暴露。养成“先跑最小例子再加功能”的习惯能把排查范围缩到最小。6. 下一步学什么从看懂第一条指令到读懂一个函数栈帧第一个例子跑通、五个坑对过一遍之后你其实已经站在门口了。接下来最忌讳的是“什么都想学”正确做法是按一条线往下走每走一步都动手验证。下面这张表是我建议的顺序左边是主题右边是学完之后你应该能回答的问题顺序主题学完能回答的问题1数据类型和寄存器一个有符号字节读进寄存器值是多少2ARM 指令集与 Thumb同一条逻辑ARM 和 Thumb 编译出来差多少字节3内存读写ldr和str的偏移量怎么写什么时候用后索引4高级内存读写多寄存器读写、块拷贝怎么写更省指令5条件分支一段 if-else 编译出来长什么样CPSR 哪一位在起作用6栈和函数一个函数的序言、主体、尾声各干了什么栈帧怎么长出来前两步是这一篇已经打下的底子第三、四步开始接触内存第五步开始读控制流第六步是把前面所有东西串起来——你会看到一个函数怎么用push {r11, lr}开场怎么用sub sp, sp, #16给自己划地方怎么在结尾用pop {r11, pc}收场。到那一步你再回头看崩溃日志里的栈回溯就不再是天书了。具体到每天怎么练我给一个很土但有效的办法每学一条新指令就在 gdb 里单步走一遍执行前后各看一次info registers把变化的那几个寄存器记下来。不要贪多一天吃透三条指令一周就是二十一条足够覆盖入门阶段的高频指令。遇到看不懂的反汇编先猜再单步验证猜错了印象反而更深。我自己的血泪经验是第一遍学的时候总想“先看懂再动手”结果看了一堆文档还是不会写。后来改成“先让最小例子跑起来再回头补概念”进度反而快了很多。从那以后我每次碰新架构都强制自己先写一个只有三条指令的退出程序跑通再往下加。这个习惯帮我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑