既然都裸机了,还是简单回顾一下汇编吧。。。
1 概念

来自:https://redfoxsec.com/blog/introduction-to-assembly-language/
汇编基本上就是机器码。汇编语言是一种直接对应处理器指令集的低级语言,它以人类可读的形式表达机器指令,是软件与硬件之间几乎最底层的一层接口;每一条汇编指令几乎都能映射为一条机器指令,能够精确控制寄存器、内存、指令顺序和硬件状态,因此被广泛用于启动代码、中断处理、上下文切换和性能或时序极端敏感的场景。
1.2 寄存器
一些必要了解的寄存器
| 寄存器 | 作用 |
|---|---|
| r0–r3 | 参数 / 返回值 |
| r4–r11 | 被调用者保存 |
| r12 | 临时 |
| sp | 栈 |
| lr | 返回地址 |
| pc | 程序计数器 |
特殊寄存器
| 寄存器 | 作用 |
|---|---|
| CPSR | 当前状态(中断 / 模式) |
| SPSR | 异常返回用 |
1.3 基础指令
| 类别 | 指令名 | 说明 | 例子 |
|---|---|---|---|
| 数据传送 | MOV | 将源操作数(寄存器或立即数)传送到目标寄存器 | MOV R0, #10 (R0 = 10) |
| MVN | 取反传送(先按位取反再传送) | MVN R0, #0 (R0 = -1) | |
| 算术运算 | ADD | 加法运算 | ADD R0, R1, R2 (R0 = R1 + R2) |
| SUB | 减法运算 | SUB R0, R1, #5 (R0 = R1 - 5) | |
| MUL | 乘法运算(注意:操作数必须是寄存器) | MUL R0, R1, R2 (R0 = R1 * R2) | |
| 逻辑运算 | AND | 按位“与” | AND R0, R0, #0xFF (只保留低8位) |
| ORR | 按位“或” | ORR R0, R0, #1 (将最低位置1) | |
| EOR | 按位“异或” | EOR R0, R1, R2 (R1和R2异或存入R0) | |
| BIC | 位清除(Bit Clear,将对应位清零) | BIC R0, R0, #0b101 (清除第0和第2位) | |
| 比较指令 | CMP | 比较两个数(本质是执行减法并更新状态标志) | CMP R0, #10 (比较R0是否等于10) |
| TST | 测试位(本质是执行按位与并更新状态标志) | TST R0, #1 (测试最低位是否为1) | |
| 内存访问 | LDR | 从内存加载数据到寄存器 (Load) | LDR R0, [R1] (将R1指向地址的值读入R0) |
| STR | 将寄存器数据保存到内存 (Store) | STR R0, [R1] (将R0的值存入R1指向的地址) | |
| 分支跳转 | B | 无条件跳转到某个标签 | B label (跳转到标签label处执行) |
| BL | 带链接跳转(保存返回地址到 LR,用于调用函数) | BL function (调用子程序) | |
| BX | 跳转并切换指令集(常用于从函数返回) | BX LR (从子程序返回) | |
| 栈操作 | PUSH | 将寄存器列表压入堆栈 | PUSH {R4, LR} (保护R4和返回地址) |
| POP | 从堆栈弹出数据到寄存器列表 | POP {R4, PC} (恢复R4并返回) |
这里MOV和LDR/STR看着有点类似,查了一下在X86,确实只有一个MOV,ARM为了提高效率,分成几个了。
2 环境搭建
实在不想本地搭建环境了。直接用web的吧,查了一下,有两个网站不错。
第一个是运行汇编的网站CPUlator:
https://cpulator.01xz.net/?sys=arm

第二个是查看C代码到汇编的映射网站Compiler Explorer (Godbolt)

最后一个是动画显示过程的。VisUAL (ARMv7 Visualizer),不过这个要安装一下。
https://salmanarif.bitbucket.io/visual/

3 一些小验证
3.1 WSL的汇编程序
hello.asm
1section .data 2 msg db "Hello", 10 ; 10 是换行符的 ASCII 码 3 4section .text 5 global _start 6 7_start: 8 ; --- 调用系统函数:write(1, msg, 6) --- 9 mov rax, 1 ; 系统调用号 1 是 sys_write 10 mov rdi, 1 ; 文件描述符 1 是 stdout (标准输出) 11 mov rsi, msg ; 字符串的地址 12 mov rdx, 6 ; 字符串长度 (H-e-l-l-o + 换行) 13 syscall ; 触发内核调用 14 15 ; --- 调用系统函数:exit(0) --- 16 mov rax, 60 ; 系统调用号 60 是 sys_exit 17 mov rdi, 0 ; 退出状态码码 0 (代表无错误) 18 syscall ; 触发内核调用
编译运行
1# 1. 将汇编代码编译为目标文件 (ELF64 格式) 2nasm -f elf64 hello.asm -o hello.o 3 4# 2. 将目标文件链接为可执行文件 5ld hello.o -o hello 6 7# 3. 运行 8./hello
很简单就出结果Hello了。
这个汇编和一般用的裸片汇编还是不同,还是运行在OS之上。地址空间应该是虚拟地址,但是寄存器是实际的寄存器。核心就是两个syscall。
这里就不细看流程了,主要是看看符号和链接。
hp@DESKTOP-430500P:~/test/asm$ readelf -h hello
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401000
Start of program headers: 64 (bytes into file)
Start of section headers: 8448 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 6
Section header string table index: 5
开始的地址是0x401000,这个是MMU的地址。有3个program headers,有6个section headers(这里就是.text .data .symtab .strtab .shstrtab存放位置)。这里的数据,有些放到RAM,有些放到FLASH,在Linux,这个是内核来做。在裸片,这部分操作要自己做。
这里的6个sections,通过命令readelf -S hello可以看到。
hp@DESKTOP-430500P:~/test/asm$ readelf -S hello
There are 6 section headers, starting at offset 0x2100:Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000401000 00001000
0000000000000027 0000000000000000 AX 0 0 16
[ 2] .data PROGBITS 0000000000402000 00002000
0000000000000006 0000000000000000 WA 0 0 4
[ 3] .symtab SYMTAB 0000000000000000 00002008
00000000000000a8 0000000000000018 4 3 8
[ 4] .strtab STRTAB 0000000000000000 000020b0
0000000000000027 0000000000000000 0 0 1
[ 5] .shstrtab STRTAB 0000000000000000 000020d7
0000000000000027 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
查看符号则是 readelf -s hello
hp@DESKTOP-430500P:~/test/asm$ readelf -s hello
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.asm
2: 0000000000402000 0 NOTYPE LOCAL DEFAULT 2 msg
3: 0000000000401000 0 NOTYPE GLOBAL DEFAULT 1 _start
4: 0000000000402006 0 NOTYPE GLOBAL DEFAULT 2 __bss_start
5: 0000000000402006 0 NOTYPE GLOBAL DEFAULT 2 _edata
6: 0000000000402008 0 NOTYPE GLOBAL DEFAULT 2 _end
这里可以看到定义的字符串,start位置,bss(未初始化变量)区,数据结束区位置。
program headers可以通过-l查看。00是文件头,01是代码段,02是数据段。
hp@DESKTOP-430500P:~/test/asm$ readelf -l hello
Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 3 program headers, starting at offset 64Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000e8 0x00000000000000e8 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000027 0x0000000000000027 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000006 0x0000000000000006 RW 0x1000Section to Segment mapping:
Segment Sections...
00
01 .text
02 .data
里面的所有符号可以用nm查看符号表。这个和readelf -s很相似,可以说是青春版。
hp@DESKTOP-430500P:~/test/asm$ nm hello
0000000000402006 D __bss_start
0000000000402006 D _edata
0000000000402008 D _end
0000000000401000 T _start
0000000000402000 d msg
最后还有一个是map文件。在link的时候用命令生成。
ld hello.o -o hello -Map hello.map
这个文件的内容很多,这里暂时就不细看了。。。
可以参考这个文章:https://mp.weixin.qq.com/s/j55ahfm3jradpmVg6hPy4g
3.2 ARM最小启动代码
start.s
1.section .vectors, "ax" 2.global _start 3_start: 4 .word 0x20080000 @ 0x00: 初始栈顶地址 (RAM 末尾) 5 .word reset_handler + 1 @ 0x04: 复位向量 (注意:Thumb 模式地址必须+1) 6 .word 0 @ 0x08: NMI 7 .word 0 @ 0x0C: HardFault 8 @ ... 后续项填充 0 或跳转 9 10.text 11reset_handler: 12 @ Cortex-M 硬件会自动加载第一个字到 SP,所以这里可以不用手动 ldr sp 13 bl main 14 b .
这里除了上面的寄存器和指令,还有一个就是标签。比如:
.global _start
_start:
.global 开头的就是全局变量,没有的就是局部变量。跟着就是标签的定义,所有的标签都不会执行,主要是给GDB调试然后跳转用。
.section开始就是指定放在哪个区。区的定义在linker那个ld文件中指定。这里所有的代码都会放到vector区。
首先就是一个向量表,起始是在reset_handler,之后设置了栈指针后就直接到main。
pico.ld
MEMORY
{
/* RP2350 的 Flash 和 RAM 地址示例 */
FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048K
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512K
}SECTIONS
{
.text : {
KEEP(*(.vectors)) /* 强制把向量表放在最前面 */
*(.text*)
} > FLASH.bss : {
*(.bss*)
} > RAM
}
最后的main最简单
int main()
{
return 0;
}
编译命令:
arm-none-eabi-gcc -x assembler-with-cpp -c -mcpu=cortex-m33 -mthumb start.s -o start.o
arm-none-eabi-gcc -c -mcpu=cortex-m33 -mthumb -O2 main.c -o main.o
arm-none-eabi-gcc start.o main.o -o project.elf -mcpu=cortex-m33 -mthumb -nostartfiles -T pico.ld -Wl,--gc-sections
首先还是readelf
E:\test\pico2\test\test>readelf readelf -h project.elf
readelf: Error: 'readelf': No such fileFile: project.elf
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x10000000
Start of program headers: 52 (bytes into file)
Start of section headers: 4544 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 40 (bytes)
Number of section headers: 7
Section header string table index: 6
可以看到,起始是0x10000000,这个和os上的区别很大。这个是RP2040 XIP Flash映射起始地址,也就是flash的起始地址。program headers只有1个,但是section headers有7个。
E:\test\pico2\test\test>readelf -S project.elf
There are 7 section headers, starting at offset 0x11c0:Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 10000000 001000 00001c 00 AX 0 0 4
[ 2] .ARM.attributes ARM_ATTRIBUTES 00000000 00101c 000030 00 0 0 1
[ 3] .comment PROGBITS 00000000 00104c 000045 01 MS 0 0 1
[ 4] .symtab SYMTAB 00000000 001094 0000c0 10 5 10 4
[ 5] .strtab STRTAB 00000000 001154 000030 00 0 0 1
[ 6] .shstrtab STRTAB 00000000 001184 00003a 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), y (purecode), p (processor specific)
作用如下:
.text 代码,主要内容都在这个部分。
.comment 注释
.ARM.attributes ARM 架构专用属性
.symtab 符号表
.strtab 符号名字符串
.shstrtab section名字表
program header在这里只有一个。
E:\test\pico2\test\test>readelf -l project.elf
Elf file type is EXEC (Executable file)
Entry point 0x10000000
There is 1 program header, starting at offset 52Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x10000000 0x10000000 0x0001c 0x0001c R E 0x1000Section to Segment mapping:
Segment Sections...
00 .text
nm的结果
E:\test\pico2\test\test>nm project.elf
10000000 t $d
10000010 t $t
10000018 t $t
10000000 T _start
10000019 T main
10000010 t reset_handler
这里T是全局符号,t是局部符号。$d是数据,$t是指令。
好了,先到这里吧。。。
《Pico裸机2(汇编基础)》 是转载文章,点击查看原文。