伪指令不是指令,伪指令和指令的根本区别是经过编译后会不会生成机器码。 伪指令的意义在于指导编译过程。 伪指令是和具体的编译器相关的,我们使用gnu工具链,因此学习gnu环境下的汇编伪指令。
在 ARM 汇编中,伪指令(Pseudoinstruction)由汇编器解析,用于辅助程序编写(如定义数据、划分段、设置对齐等),不直接生成机器码。以下是常用伪指令及其用法示例:
一、数据定义伪指令
用于在内存中定义数据(如整数、字符串、数组等)。
1.
.byte
定义8 位整数(1 字节),可跟多个值,用逗号分隔。
.data byte1: .byte 0x10 ; 定义1字节数据0x10 byte2: .byte 0x20, 0x30 ; 连续定义两个字节:0x20、0x30
2.
.half
/.short
定义16 位整数(2 字节),两者功能相同。
.data half1: .half 0x1234 ; 定义2字节数据0x1234 half2: .short 0x5678 ; 等价于.half,定义0x5678
3.
.word
/.int
定义32 位整数(4 字节),ARM 架构中最常用的数据定义伪指令。
.data word1: .word 0x12345678 ; 定义4字节数据0x12345678 array: .word 1, 2, 3 ; 定义32位整数数组:1、2、3(占12字节)
4.
.quad
/.long
定义64 位整数(8 字节),适用于支持 64 位的 ARM 架构(如 ARMv8)。
.data quad1: .quad 0x1122334455667788 ; 定义8字节数据
5.
.asciz
/.ascii
.asciz
:定义以 null 结尾的字符串(C 风格字符串,自动添加\0
)。.ascii
:定义字符串,不自动添加 null 结尾。.data str1: .asciz "Hello" ; 存储 'H','e','l','l','o','\0'(6字节) str2: .ascii "ARM" ; 存储 'A','R','M'(3字节,无结束符)
6.
.space
/.fill
.space n
:分配n 字节的未初始化空间。.fill n, size, val
:重复填充 n 次,每次填充 size 字节,值为 val(size 通常为 1、2、4)。.data buf: .space 100 ; 分配100字节缓冲区 arr: .fill 5, 4, 0x1234 ; 填充5个4字节数据,每个值为0x1234(共20字节)
更多待补充
数据只能在.data段下定义吗?
在 ARM 汇编中,数据并非只能在
.data
段中定义,不同类型的数据适合放在不同的段中,常见的段包括.data
、.bss
、.rodata
等,各自有不同的用途和特性。1.
.data
段:初始化的读写数据
- 用途:存放已初始化且需要修改的数据(如变量、数组等)。
- 特点:数据会被存储在可执行文件中,加载到内存后可读写。
- 示例:
.data count: .word 100 ; 初始化的整数(可修改) buffer: .byte 0x01, 0x02 ; 初始化的字节数组(可修改)
2.
.bss
段:未初始化或零初始化数据
- 用途:存放未初始化或初始值为 0的数据(如缓冲区、大数组等)。
- 特点:
- 不占用可执行文件的空间(仅记录需要分配的大小)。
- 程序加载时由系统自动初始化为 0。
- 适合存储大容量数据(节省文件大小)。
- 示例:
.bss big_buffer: .space 1024 ; 1024字节未初始化缓冲区(自动初始化为0) temp_var: .word 0 ; 初始值为0的变量(等效于未初始化)
3.
.rodata
段:只读数据
- 用途:存放初始化后不需要修改的数据(如常量、字符串字面量等)。
- 特点:
- 数据会被存储在可执行文件中。
- 加载到内存后处于只读区域(修改会触发错误)。
- 提高安全性,适合保护常量数据。
- 示例:
.rodata PI: .word 0x4048F5C3 ; 浮点数π的32位表示(只读) msg: .asciz "Hello" ; 只读字符串(不可修改)
4. 自定义段(.section)
通过
.section
伪指令可创建自定义段,用于特殊用途(如硬件相关数据、特定内存区域的数据等)。示例:定义一个用于硬件寄存器映射的数据段
.section .hw_regs, "aw" ; "a"=可分配, "w"=可写 uart_base: .word 0x12340000 ; UART寄存器基地址(自定义段数据)
总结:如何选择段?
数据类型 推荐段 原因 已初始化且需要修改 .data
可读写,数据存储在文件中 未初始化或初始值为 0 .bss
不占文件空间,自动初始化为 0 初始化后不需要修改 .rodata
只读保护,防止意外修改 特殊用途(如硬件相关) 自定义段 灵活控制段属性和内存布局 合理划分数据段可以优化程序的内存使用效率,并提高代码的安全性和可维护性。
如何访问数据
用伪指令定义数据后,怎么访问这个数据?
在 ARM 汇编中,访问用伪指令定义的数据通常需要通过标号(Label) 获取数据地址,再结合加载 / 存储指令(如
LDR
、STR
)访问数据。具体方法取决于数据的类型和位置,以下是常见场景及示例:一、访问数据段中的初始化数据
假设已用伪指令在
.data
段定义数据,通过标号获取地址后,用LDR
(加载)指令读取数据到寄存器。示例 1:访问 32 位整数(.word 定义)
.data ; 用伪指令定义数据并标记标号 num: .word 0x12345678 ; 32位数据,地址由标号num标识.text .global main main:; 步骤1:将数据地址加载到寄存器(用LDR =标号)LDR R0, =num ; R0 = num的地址(0x...); 步骤2:从地址加载数据到寄存器(用LDR 寄存器, [地址寄存器])LDR R1, [R0] ; R1 = 内存[R0] = 0x12345678; 后续操作(例如修改数据后存回)ADD R1, R1, #1 ; R1 = 0x12345679STR R1, [R0] ; 内存[R0] = R1(更新数据); 程序结束MOV PC, LR
示例 2:访问字符串(.asciz 定义)
字符串本质是字节数组,可通过地址逐个访问字符:
.data str: .asciz "Hello" ; 字符串:'H','e','l','l','o','\0'.text main:LDR R0, =str ; R0 = 字符串首地址; 访问第一个字符(H的ASCII码为0x48)LDRB R1, [R0] ; R1 = 0x48(LDRB:加载字节,自动零扩展); 访问第二个字符(e的ASCII码为0x65)LDRB R2, [R0, #1] ; R2 = 0x65(地址偏移+1字节)MOV PC, LR
二、访问数组数据(多个连续元素)
对于
.word
、.byte
等定义的数组,通过地址 + 偏移量访问元素:.data arr: .word 10, 20, 30, 40 ; 32位整数数组(每个元素4字节).text main:LDR R0, =arr ; R0 = 数组首地址; 访问第1个元素(索引0):10LDR R1, [R0] ; R1 = 10; 访问第2个元素(索引1):20(偏移4字节)LDR R2, [R0, #4] ; R2 = 20; 访问第3个元素(索引2):30(用寄存器偏移)MOV R3, #8LDR R4, [R0, R3] ; R4 = 30MOV PC, LR
三、访问.bss 段的未初始化数据
.bss
段数据仅分配空间(初始值为 0),访问方式与.data
段相同:.bss buf: .space 16 ; 16字节未初始化缓冲区.text main:LDR R0, =buf ; R0 = 缓冲区首地址; 向缓冲区第1个字节写入0x55MOV R1, #0x55STRB R1, [R0] ; STRB:存储字节; 向缓冲区第5个字节(偏移4)写入0xAAMOV R2, #0xAASTRB R2, [R0, #4]MOV PC, LR
四、关键指令总结
指令 用途 示例 LDR Rn, =label
将标号地址加载到寄存器 LDR R0, =num
LDR Rd, [Rn]
从 Rn 指向的地址加载 32 位数据 LDR R1, [R0]
LDRB Rd, [Rn]
从 Rn 指向的地址加载 8 位数据 LDRB R1, [R0]
LDRH Rd, [Rn]
从 Rn 指向的地址加载 16 位数据 LDRH R1, [R0]
STR Rd, [Rn]
将 32 位数据存储到 Rn 指向的地址 STR R1, [R0]
STRB Rd, [Rn]
将 8 位数据存储到 Rn 指向的地址 STRB R1, [R0]
STRH Rd, [Rn]
将 16 位数据存储到 Rn 指向的地址 STRH R1, [R0]
核心逻辑
- 获取地址:用
LDR Rn, =label
将数据标号的地址加载到寄存器(Rn)。- 访问数据:根据数据宽度(8/16/32 位),用
LDR
/STR
及其变体(LDRB
/STRB
等),通过地址寄存器(Rn)读写内存。这种方式依赖汇编器对地址的解析和链接器的地址分配,最终实现对伪指令定义数据的访问。
二、段定义伪指令
用于划分程序的不同段(代码段、数据段等),便于内存管理。
1.
.text
定义代码段(存放可执行指令),是程序的默认段。
.text ; 进入代码段 start:MOV R0, #0 ; 指令存放于此段
2.
.data
定义数据段(存放初始化的数据)。
.data ; 进入数据段 var: .word 0x1234 ; 初始化数据存放于此段
3.
.bss
定义未初始化数据段(仅分配空间,初始值为 0),节省可执行文件大小。
.bss ; 进入未初始化数据段 buf: .space 200 ; 200字节缓冲区(初始为0)
4.
.section
自定义段(灵活划分特殊用途的段,如中断向量表)。
.section .vector, "a" ; 定义名为.vector的段,"a"表示可分配 reset:B start ; 中断向量表中的复位向量
更多待补充
三、符号与地址伪指令
用于声明符号可见性、加载地址等。
1.
.global
/.extern
.global sym
:声明 sym 为全局符号(可被其他文件引用)。.extern sym
:声明 sym 为外部符号(在其他文件中定义)。.global main ; 声明main为全局符号(供链接器识别) .extern printf ; 声明printf为外部符号(来自C库)
2.
.equ
定义符号常量(类似 C 中的
#define
),便于代码维护。.equ MAX_LEN, 100 ; 定义常量MAX_LEN=100 .equ PI, 3.14 ; 也可定义浮点数(汇编器支持时).text start:MOV R1, #MAX_LEN ; 使用常量
3.
=label
(地址加载伪指令)配合
LDR
指令,将标号的绝对地址加载到寄存器(实际会被汇编器转换为合适的指令)。.data msg: .asciz "Hello".text main:LDR R0, =msg ; 将msg的地址加载到R0(等价于加载绝对地址)LDR R1, =0x12345678 ; 加载32位立即数(超出MOV指令范围时用)
更多待补充
四、对齐与定位伪指令
用于控制数据或指令在内存中的对齐方式(提高访问效率)。
1.
.align n
使当前地址对齐到
2^n
字节边界(n 通常为 0~3,对应 1、2、4、8 字节对齐)。.data .align 2 ; 对齐到4字节边界(2^2=4) val: .word 0x1234 ; val的地址必为4的倍数
2.
.org addr
强制将当前地址设置为
addr
(常用于固定地址初始化,如硬件寄存器)。.org 0x40000000 ; 强制当前地址为0x40000000(假设为UART寄存器地址) uart_tx: .word 0 ; uart_tx的地址固定为0x40000000
更多待补充
五、其他常用伪指令
1.
.end
标记程序结束,汇编器遇到此指令后停止处理。
.text start:MOV PC, LR .end ; 程序结束
2.
.include
包含其他汇编文件(类似 C 的
#include
),便于代码复用。.include "common.s" ; 包含common.s文件中的代码
3.
.thumb
/.arm
切换指令集:
.thumb
进入 Thumb 模式(16/32 位指令),.arm
进入 ARM 模式(32 位指令)。.arm ; 使用ARM指令集 MOV R0, #1.thumb ; 切换到Thumb指令集 MOV R1, #2
更多待补充
总结
伪指令是 ARM 汇编的 “辅助工具”,核心作用是:
- 定义数据(
.word
、.asciz
等);- 划分内存段(
.text
、.data
等);- 控制符号与地址(
.global
、=label
等);- 优化内存对齐(
.align
)。灵活使用伪指令可使汇编代码更清晰、易维护,同时适配不同的硬件和内存布局需求。