资讯详情

ARM汇编入门:工具链、寄存器、CPSR与GDB调试

📅 2026/10/26 16:21:14 | 华诺云谱 👁 阅读
ARM汇编入门:工具链、寄存器、CPSR与GDB调试
简介面向初学ARM汇编、准备进入底层开发或逆向工程领域的读者这份教程文档从处理器版本与指令集的对应关系讲起梳理汇编链接流程.s经as生成.o再由ld链接为可执行程序、有符号与无符号的字/半字/字节数据类型、大小端字节序的控制方式以及R0-R15通用与特殊功能寄存器的分工包括SP栈指针、LR返回地址保存、PC流水线偏移和CPSR状态位等易混淆细节。文档还给出ARM函数调用约定中前四个参数存入R0-R3的规则并附参考手册、Raspberry Pi汇编实践与逆向工程书目等拓展阅读线索。从寄存器编号含义到流水线导致的PC偏移现象原文均配以示例指令与调试器观察结果读者可据此理解汇编指令的实际执行过程为后续阅读shellcode编写与漏洞分析材料打下基础。资源包内为1个docx文件约2.65MB结构紧凑、主题集中适合作为入门笔记与案头速查材料目前已有209人学习。1. ARM汇编基础教程1值不值得拆七篇目录里的三条主线手头这份《ARM汇编基础教程1》不是单篇长文而是七篇连起来的ARM汇编入门线从处理器版本与指令集映射、as/ld汇编链接到数据类型、R0-R15寄存器、ARM/Thumb指令集、内存读写、条件分支、栈与函数。它最适合已经会一点C、准备进入嵌入式调试或底层逆向的从业者也适合刚接触ARM汇编、需要一套能对照调试器复现的新手。真正有价值的地方不是指令表而是把PC流水线偏移、CPSR标志位、栈帧prologue/epilogue这些容易翻车的点拆开讲。读之前先明确这份材料不能替代工具链实操必须边汇编边用GDB看寄存器否则很容易停在“看得懂、写不出”的状态。2. 从.s到可执行文件as/ld链路、交叉工具链与qemu验证2.1 工具链选型为什么不能拿x86的as直接汇编教程原文给的命令是as program.s -o program.o和ld program.o -o program这两条在ARM原生环境或目标板上成立在x86开发机上直接敲调用的却是x86汇编器轻则报错重则产出一个架构不对的ELF文件后面用qemu跑的时候才发现根本跑不起来。常见做法是装交叉工具链用arm-linux-gnueabihf-as和arm-linux-gnueabihf-ld让汇编器和链接器都按ARM目标生成代码。gnueabihf这一套带硬浮点ABI适合大多数带FPU的ARM Linux用户态实验如果目标是裸机或更小的处理器就换对应工具链别拿一套包硬套所有板子。qemu-user在这里的角色是“指令行为验证器”不是完整开发板。它跑用户态静态ELF足够看寄存器、栈和系统调用但不要指望它模拟所有外设。安装和第一次汇编可以照下面这条链路走包名按你的发行版仓库调整命令里的架构参数按目标改。sudo apt install gcc-arm-linux-gnueabihf qemu-user gdb-multiarch arm-linux-gnueabihf-as -marcharmv7-a program.s -o program.o arm-linux-gnueabihf-ld program.o -o program file program qemu-arm ./program-marcharmv7-a是在告诉汇编器按哪个指令集版本选编码目标板是ARMv6就换成对应参数不要照抄-o指定输出文件.o是目标文件不带-o会默认写成a.outfile program用来确认Machine字段是不是ARM这一步比急着运行更省时间。qemu-arm直接跑静态链接的ELF退出码可以用echo $?看后面排查入口问题时这个退出码很有用。2.2 最小可执行骨架_start、退出系统调用和链接入口教程里的例子经常写main然后用bx lr返回这在调试器里看寄存器没问题但裸链接成可执行文件时容易踩入口的坑。ld默认找_start找不到就警告并猜一个入口程序跑起来可能直接段错误。更稳的做法是先写一个最小_start骨架用ARM EABI的退出系统调用收尾确认工具链、链接和qemu这条链路是通的再回去看教程里的main样例。.syntax unified .arch armv7-a .text .global _start _start: mov r0, #0 退出码 mov r7, #1 ARM EABI 退出系统调用号 svc #0arm-linux-gnueabihf-as -marcharmv7-a program.s -o program.o arm-linux-gnueabihf-ld program.o -o program file program readelf -h program | grep Entry qemu-arm ./program echo $?.syntax unified让汇编器接受统一语法后面写Thumb指令时不用来回切语法.arch声明目标架构和汇编命令里的-march保持一致.text是代码段.global _start把入口符号导出ld才能找到它mov r7, #1是退出系统调用号svc #0触发系统调用。readelf -h里的Entry point address能告诉你入口到底落在哪echo $?返回0说明这条最小链路已经通了。注意这里没有libc不能调用printf想看输出得自己走write系统调用或者上gdb看寄存器。2.3 用qemu-arm的gdb stub单步把黑匣子打开只看反汇编和寄存器打印很多细节对不上尤其是PC读出来的值和教材差一截。qemu-arm自带gdb stub可以让gdb-multiarch连上去单步这样每条指令执行前后都能看到R0-R15和CPSR的变化。启动时让qemu在端口上等gdb连上去先断在入口再stepi逐条走。qemu-arm -g 1234 ./program gdb-multiarch -q ./program \ -ex set architecture arm \ -ex target remote :1234 \ -ex break _start \ -ex continue \ -ex info registers \ -ex x/4i \$pc-g 1234让qemu在1234端口开gdb stub端口被占用就换一个set architecture arm显式告诉gdb目标架构避免它按主机架构解析寄存器target remote连的是qemu的stub不是本地进程break _start断在入口info registers一次看全寄存器x/4i $pc看当前PC附近的指令。Thumb代码的断点地址要注意最低位gdb里显示的是去掉最低位后的地址别被这个细节绕进去。调试时先确认$pc和x/i $pc指向同一条指令再往下单步。3. 寄存器与CPSR怎么设参数R0-R15分工、PC偏移和N/Z/C/V实测3.1 R0-R15的分工函数调用约定不是“随便用”ARM寄存器数量看版本基础系列重点盯R0-R15这16个。R0-R3用来传前四个参数也用来放返回值R4-R11是通用寄存器但按调用约定属于被调用者保存函数里要用就得先push再popR12是IP内部程序调用寄存器链接器有时拿它做跳板R13是SP栈指针R14是LR保存函数返回地址R15是PC程序计数器。把这些分工记住后面看函数序言和尾声才不会乱。寄存器别名典型用途调用约定R0-R3参数/返回值前四个参数、返回值调用者保存R4-R11通用局部变量、栈帧指针被调用者保存R12IP内部调用临时寄存器调用者保存R13SP栈顶指针特殊R14LR返回地址特殊R15PC当前指令地址特殊CPSR状态寄存器N/Z/C/V/E/T/M等特殊写一个四参数加法函数能直接看到R0-R3怎么进、返回值怎么出。bl会把下一条指令的地址写进LRbx lr从LR跳回去如果函数里改了R4-R11必须自己保存和恢复否则调用者原来的值就被覆盖了。.syntax unified .text .global main main: mov r0, #1 mov r1, #2 mov r2, #3 mov r3, #4 bl add4 bx lr add4: add r0, r0, r1 add r0, r0, r2 add r0, r0, r3 bx lrmov r0, #1到mov r3, #4是在准备前四个参数bl add4跳转并把返回地址写进LRadd4里只改R0没有动R4-R11所以不需要额外保存最后bx lr返回。超过四个参数时剩余参数走栈返回值仍然优先放R064位返回值会用R0和R1组合。注意main被调用时LR已经由调用者设置好裸写main再用bx lr返回在某些运行环境下会跑飞这也是后面避坑章要展开的点。3.2 PC读出来为什么不是当前指令8/4与三级流水线教程里有一段很典型的实验mov r0, pc执行后R0里的值不是当前指令地址而是当前指令地址加8。ARM状态下读PC得到当前指令地址8Thumb状态下得到4。原文给了两种解释一种说老款处理器取的是后面两条指令的地址一种说调试器显示经过处理实际调试时按三级流水线理解更稳取指、译码、执行使用不同硬件部件并行推进执行当前指令时PC已经指向后面取指的位置。调试器为了展示逻辑性可能把PC显示成当前指令地址所以你会看到$pc和mov r0, pc读出来的值差一截。.syntax unified .text .global main main: mov r0, pc mov r1, #2 add r2, r1, r1 bx lrarm-linux-gnueabihf-as -marcharmv7-a pc.s -o pc.o arm-linux-gnueabihf-ld -e main pc.o -o pc qemu-arm -g 1234 ./pc gdb-multiarch -q ./pc \ -ex set architecture arm \ -ex target remote :1234 \ -ex break main \ -ex continue \ -ex x/4i \$pc \ -ex stepi \ -ex info registers pc r0x/4i $pc先看断点落在哪条指令stepi单步一条机器指令info registers pc r0看执行后的PC和R0。你会发现R0和断点地址之间差8这不是qemu算错而是ARM读PC的约定。Thumb状态下这个偏移变成4所以同一段逻辑在ARM和Thumb下读出来的值不一样。调汇编时别拿教材里的固定地址硬套先看当前状态和指令地址再按8或4换算。3.3 CPSR标志位CMP之后到底看哪一位CPSR里最常打交道的是一组条件标志N表示结果为负Z表示结果为零C表示进位或无借位V表示有符号溢出E控制大小端T控制ARM/Thumb状态M是处理器模式位。CMP指令做的是隐式减法不改变操作数只更新CPSR。比较1和2时1-2为负N置1比较2和1时2-1没有借位C置1比较2和2时结果为0Z置1同时C也置1。这里最容易错的是C在减法里的含义它表示“没有借位”不是“有借位”。标志位含义典型触发N结果为负1-2Z结果为零2-2C加法进位/减法无借位2-1V有符号溢出结果超出32位补码范围E大小端0小端1大端T指令状态1 Thumb0 ARMM处理器模式5位模式编码JJazelle部分处理器支持.syntax unified .text .global main main: mov r1, #1 mov r2, #2 cmp r1, r2 1-2N1 cmp r2, r1 2-1C1 cmp r2, r2 2-2Z1且C1 bx lrqemu-arm -g 1234 ./cpsr gdb-multiarch -q ./cpsr \ -ex set architecture arm \ -ex target remote :1234 \ -ex break main \ -ex continue \ -ex stepi \ -ex p/x \$cpsr \ -ex info registers cpsrstepi逐条走过三条CMPp/x $cpsr看十六进制值info registers cpsr看gdb解析出来的标志位。GDB里寄存器名就是$cpsr不要写成$apsr不同调试器命名有差异。看标志位时先盯最高几位再对照N/Z/C/V别被低位模式位干扰。条件执行和循环指令都靠这些位支配后面写moveq、movne时先确认CMP确实更新了你以为的那一位。4. ARM与Thumb切换BX/BLX最低位、条件执行和桶形移位器4.1 ARM状态和Thumb状态32位与16位指令的选择ARM处理器有ARM和Thumb两种工作状态和处理器运行模式不是一回事。ARM状态指令长度32位Thumb状态指令长度16位Thumb-2里也可以有32位指令。Thumb-1用于ARMv6及更早版本Thumb-2是对Thumb-1的扩展增加了更多指令并允许16位或32位宽度ThumbEE又在Thumb-2基础上加了动态代码生成相关的变更。写shellcode时常见做法是用Thumb指令代替32位ARM指令因为16位指令更短能减少指令中出现\0截断的概率但Thumb的条件执行不如ARM状态自由某些版本要借助IT指令。切换状态靠BX或BLX目标寄存器的最低位决定切换方向最低位置1进Thumb置0回ARM。处理器取指时忽略最低位所以标签地址1不会造成对齐异常。下面这段代码用adr取标签地址加1后bx过去标签前用.thumb和.thumb_func标记Thumb代码。.syntax unified .text .global main main: adr r0, thumb_entry add r0, r0, #1 最低位置1切到Thumb bx r0 .thumb .thumb_func thumb_entry: mov r1, #1 bx lradr r0, thumb_entry取的是标签的相对地址add r0, r0, #1把最低位设成1bx r0根据最低位切状态.thumb告诉汇编器后面的指令按Thumb编码.thumb_func让链接器知道这是一个Thumb函数入口不然BL过去可能只跳到奇数地址而没有正确设置状态。返回时如果要从Thumb回ARM目标地址最低位要清0。这个最低位不是“地址错了”而是状态切换开关调试时看到奇数地址不要慌。4.2 条件执行ARM全指令可带条件Thumb要看ITARM状态下几乎所有指令都可以带条件后缀指令执行前先校验CPSR里的标志位条件匹配才执行。这样能减少分支指令代码密度也更好。Thumb状态下条件执行受限有些版本用IT指令构造条件块IT后面跟的指令要带匹配的条件后缀不同版本对IT的支持还不一样写之前先确认目标版本。.syntax unified .text .global main main: mov r0, #0 cmp r0, #0 moveq r1, #1 movne r1, #2 bx lrcmp r0, #0更新Z标志moveq r1, #1只在Z1时执行movne r1, #2只在Z0时执行。条件后缀EQ/NE/GT/LT这些不是函数名是指令的一部分写错后缀汇编器会直接报错。Thumb下可以写成it eq加moveqit ne加movne注意IT块里的指令数和条件要匹配别在IT块里插无关指令。.thumb .thumb_func thumb_cmp: cmp r0, #0 it eq moveq r1, #1 it ne movne r1, #2 bx lrit eq开启一个条件块后面一条moveq在Z1时执行it ne同理。IT指令在不同ARM版本中的行为有差异有的版本后来把它删了有的版本只支持有限组合。遇到汇编器报IT相关错误先查目标架构支持不支持再决定是用条件分支还是换成ARM状态写。4.3 桶形移位器第二操作数里做移位ARM指令的第二操作数可以是立即数、寄存器也可以是带移位的寄存器。桶形移位器让移位和算术逻辑指令合并成一条比如左移一位直接写在MOV里不用先移位再MOV。这个特性在ARM状态很常见写循环和地址计算时能省指令。第二操作数的移位形式包括LSL逻辑左移、LSR逻辑右移、ASR算术右移、ROR循环右移、RRX带扩展的循环右移一位。.syntax unified .text .global main main: mov r0, #3 mov r1, r0, lsl #1 r1 r0 * 2 add r2, r1, r0, lsr #1 r2 r1 r0/2 cmp r0, r1, lsl #2 bx lrmov r1, r0, lsl #1把R0左移一位后放进R1等价于乘2add r2, r1, r0, lsr #1在加法里对R0做逻辑右移移位不单独占一条指令cmp r0, r1, lsl #2比较R0和R1左移两位后的值。立即数移位受编码限制不是任意数都能塞进去寄存器移位更灵活但要注意移位量来自寄存器时只取低8位。调试时如果结果和手算对不上先看第二操作数的移位类型和位数再看CPSR里的C位有没有被移位影响。5. 避坑排查汇编样例跑不通时先看这五处5.1 先确认文件和入口ELF架构、入口符号与静态链接汇编样例跑不通第一反应不应该是改指令而是确认你手里这个ELF到底是什么架构、入口在哪、是不是静态链接。教程里的as和ld命令在ARM原生环境成立在x86开发机上直接敲会调用主机工具链产出的ELF Machine字段不是ARMqemu自然不认。先跑file和readelf -h比盯着代码猜快得多。现象一qemu-arm: Invalid ELF image for this architecture或者Exec format error。原因用了x86的as/ld或者用gcc动态链接后qemu找不到目标机的动态加载器。解决用交叉工具链重新汇编链接裸ld默认静态如果走gcc就加-static先file program看Machine是不是ARM。file program readelf -h program | grep -E Class|Machine|Entry readelf -s program | grep -E _start|mainfile看整体架构readelf -h看ELF头和入口地址readelf -s看符号表里有没有_start或main。如果Machine显示x86-64说明工具链用错了如果入口地址是0或者一个奇怪的值说明链接器没找到入口符号。现象二ld: warning: cannot find entry symbol _start; defaulting to ...运行时段错误。原因只写了mainld默认找_start或者main里用bx lr返回时LR没有合法地址。解决要么写_start并用退出系统调用收尾要么链接时用-e main显式指定入口同时确保LR有值。arm-linux-gnueabihf-ld -e main program.o -o program-e main把入口符号指定为main适合调试单个函数但main里的bx lr仍然要求LR指向合法返回地址裸跑时LR可能是0所以更稳的是在_start里设置好再调main或者直接用退出系统调用结束。不要看到-e main能链接就以为运行一定没问题入口和返回路径要一起看。5.2 再看状态位和栈PC偏移、BX最低位、push/pop配对入口没问题之后第二类翻车集中在状态和栈。PC读出来和教材差8或4BX跳过去没进Thumbpush/pop之后SP对不上函数返回跑飞这些现象背后往往不是玄学而是几个固定规则没对上。先把当前指令地址、CPSR的T位、SP的值打出来再逐条对照。现象三GDB里$pc和mov r0, pc读出来的值差8或4和教材对不上。原因ARM状态读PC得到当前指令地址8Thumb状态4三级流水线让PC在执行当前指令时已经指向后面取指的位置调试器显示可能经过处理。解决以x/i $pc为准按当前状态换算不要拿固定地址硬套。gdb-multiarch -q ./pc \ -ex set architecture arm \ -ex target remote :1234 \ -ex x/4i \$pc \ -ex info registers pc r0 cpsrx/4i $pc看当前PC附近的指令info registers pc r0 cpsr同时看PC、R0和CPSR。CPSR的T位告诉你当前是ARM还是Thumb换算偏移时先看这一位。断点刚命中时PC可能停在断点指令上单步之后再看读PC的结果顺序别搞反。现象四bx r0之后没进Thumb或者直接跑飞。原因目标地址最低位没置1或者Thumb函数没有用.thumb_func标记链接器按ARM入口处理。解决跳转前add r0, r0, #1Thumb标签前加.thumb和.thumb_func返回时目标地址最低位清0。.thumb .thumb_func thumb_entry: mov r1, #1 bx lr.thumb切汇编模式.thumb_func标记函数入口两样缺一不可。最低位是状态开关不是地址的一部分处理器取指时会忽略它。调试时如果发现跳过去还是ARM状态先看CPSR的T位再看BX的目标寄存器最低位。现象五pop {r0}之后SP对不上或者函数返回跑飞。原因栈是full descendingpush先减SP再存pop先读再增SP保存LR和恢复PC不匹配或者push/pop没有成对。解决按栈帧成对写push {r11, lr}和pop {r11, pc}检查SP是否4字节对齐。push {r11, lr} add r11, sp, #0 sub sp, sp, #8 body mov sp, r11 pop {r11, pc}push {r11, lr}保存帧指针和返回地址add r11, sp, #0把帧指针设到栈帧底部sub sp, sp, #8给局部变量开8字节空间收尾时mov sp, r11先恢复SPpop {r11, pc}把保存的LR直接弹进PC这样函数返回不靠额外的bx lr。如果中间改了R4-R11也要在push列表里保存pop列表里按相反顺序恢复。栈不对齐时有些指令会直接异常别忽略SP的低两位。5.3 排查流程先反汇编再单步最后看系统调用排查顺序建议固定下来先file和readelf确认ELF再objdump -d看反汇编和地址最后用qemu的gdb stub单步看寄存器。系统调用相关的问题可以加-strace它会把程序发起的系统调用和返回值打出来比盲猜快。arm-linux-gnueabihf-objdump -d program | sed -n 1,40p qemu-arm -strace ./programobjdump -d反汇编代码段sed -n 1,40p只看前40行先确认入口附近的指令和地址qemu-arm -strace打印系统调用退出码不对时看最后一条系统调用是什么。反汇编里的地址和GDB里的地址要对得上对不上就先怀疑ELF架构或链接脚本。排查时一次只改一个变量改完重新汇编链接别在一条命令里叠多个修改否则出了问题不知道是哪一步引入的。6. 把样例跑成可验证证据GDB单步、反汇编与状态位对照6.1 用GDB命令脚本抓状态快照单步几条指令手工看寄存器容易漏写成GDB命令脚本能固定抓PC、CPSR、SP、LR四个值。下面这段脚本连上qemu stub断在main连续走8条指令每条打印一次状态。$i是gdb自己的便利变量不会写进目标寄存器printf里的%#x按十六进制输出\\n在命令行里转义成换行。qemu-arm -g 1234 ./program gdb-multiarch -q ./program \ -ex set architecture arm \ -ex target remote :1234 \ -ex break main \ -ex continue \ -ex set \$i 0 \ -ex while \$i 8 \ -ex stepi \ -ex printf \pc%#x cpsr%#x sp%#x lr%#x\\n\, \$pc, \$cpsr, \$sp, \$lr \ -ex set \$i \$i 1 \ -ex endstepi单步一条机器指令不是一行源码$cpsr看标志位和T位$sp看栈有没有按预期减4或加4$lr看BL有没有写返回地址。8条足够覆盖一个小函数的前半段遇到分支再单独加断点。打印出来的PC如果和x/i $pc对不上先检查是不是Thumb状态Thumb下地址最低位在gdb显示里可能被隐藏。6.2 反汇编与源码对照的检查表每跑完一个样例用下面这张表对一遍比反复重启调试器省时间。检查项不用全做但入口、架构、状态位、栈指针这四项建议每次都过。检查项命令期望ELF架构file program显示ARM入口地址readelf -h programEntry非0且指向代码反汇编入口objdump -d第一条指令与源码一致当前状态p/x $cpsrT位与预期一致栈指针info registers sp4字节对齐返回地址info registers lrBL之后指向下一条指令这套检查流程不花哨但能省掉很多玄学时间。从那以后我每次拿到汇编样例都先跑file和readelf -h再在GDB里单步八条确认PC偏移、CPSR的T位和SP对齐之后再改代码避免把工具链问题当成指令写错。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑