GNU汇编基础
1. 概述
GNU汇编器(AS)是GNU工具链的组成部分,采用跨平台的统一语法。在嵌入式开发中,它常用于处理器初始化、中断处理等底层硬件操作。本文以ARM Cortex-A7为例,讲解GNU汇编的核心语法与实战应用。
2. 基础语法规则
2.1 语句结构
GNU 汇编每行一条语句,格式固定为 label: instruction @ comment,三部分均为可选(无标号则省略 “label:”,无注释则省略 “@ comment”)。
- label(标号):表示地址位置,用于跳转、函数入口或数据地址,必须以
:结尾(如_start:、LED_Init:)。 - instruction(指令):可是 ARM 汇编指令(如
MOV、LDR)或伪操作(如.global、.section)。 - comment(注释):用
@开头(单行),或/* ... */(多行),作用同 C 语言注释,仅用于说明,不参与编译。
示例:
_start:
MOV R0, #0x12 @ 设置 R0 = 0x12
ADD R1, R0, #1 /* R1 = R0 + 1 */
2.2 大小写规范
- 指令、伪指令、寄存器等可以全部大写或小写
- 禁止大小写混用
- 推荐使用小写以提高可读性
3. 段定义与常用伪操作
GNU 汇编通过 “段” 划分代码、数据区域,需用伪操作(以 . 开头的指令)定义,伪操作仅指导汇编器编译,不生成机器码。
3.1 预定义段(核心必用)
汇编器预定义了 4 个核心段,无需自定义,直接用 .section 声明即可,各段作用如下:
| 段名 | 作用 | 特点 |
|---|---|---|
.text |
存放代码(指令) | 只读,加载到内存可执行区域 |
.data |
存放已初始化的数据(如全局变量) | 可读写,占用 Flash 和内存空间 |
.bss |
存放未初始化的数据 | 仅占内存空间,Flash 中不存储(上电清零) |
.rodata |
存放只读数据(如常量、字符串) | 只读,可优化到 .text 段节省空间 |
示例:定义段
.section .text @ 声明当前代码属于代码段
.global _start @ 导出_start为全局符号(链接器需此作为入口)
_start: @ 程序入口标号(链接脚本需指定ENTRY(_start))
.section .data @ 声明数据段
num1: .long 0X12345678 @ 已初始化数据:4字节(long)变量num1=0X12345678
.section .bss @ 声明未初始化数据段
buf: .space 1024 @ 分配1024字节空间(buf为起始地址,上电后值为0)
3.2 常用伪操作
| 伪操作 | 格式 | 作用 | 示例 |
|---|---|---|---|
.global |
.global symbol |
导出符号(让链接器 / 其他文件可见) | .global LED_On(导出 LED_On 函数) |
.equ |
.equ 变量名, 表达式 |
定义常量(类似 C 的 #define) | .equ GPIO1_ADDR, 0X0209C004 |
.align |
.align 对齐字节数 |
数据 / 代码地址对齐(需 2 的幂,如 4/8) | .align 4(4 字节对齐,避免硬件错误) |
.byte/.short/.long |
.byte 数值 |
定义 1/2/4 字节数据 | .byte 0XAB(1 字节)、.long 0X1234(4 字节) |
.space |
.space 长度, 填充值 |
分配指定字节空间(填充值默认 0) | .space 8(分配 8 字节,值为 0) |
.end |
.end |
标识源文件结束(可省略,编译器自动识别) | 无 |
4. 函数设计与调用规则
GNU 汇编函数需遵循 ATPCS 规则(ARM 函数调用标准),核心是 “寄存器使用约定” 和 “现场保护”,否则会导致数据混乱或函数无法返回。
4.1 函数基本结构
汇编函数由 “函数标号 + 函数体 + 返回指令” 组成,返回指令需根据场景选择(BX LR 或 POP {..., PC})。
/* 函数名:Add(实现 a + b)
输入:R0 = a,R1 = b
输出:R0 = a + b
*/
Add:
ADD R0, R0, R1 @ 函数体:R0 = R0 + R1(利用输入寄存器存结果)
BX LR @ 返回指令:将LR的值赋给PC,回到调用处
4.2 关键规则:现场保护与恢复
现场保护的核心是 “被调用者保存寄存器” 需压栈(避免修改后影响调用者),ATPCS 规则明确:
- 调用者保存寄存器:R0~R3、R12(调用者负责保存,被调用者可直接修改);
- 被调用者保存寄存器:R4~R11、LR(被调用者若使用,必须压栈保存,退出时恢复)。
示例:带现场保护的函数
/* 需要保护现场的复杂函数示例 */
Complex_Func:
push {r4-r6, lr} @ 保存非易失寄存器
mov r4, r0 @ 使用r4(非易失,已保护)
bl Some_Sub_Func @ 调用子函数(会修改lr)
add r0, r4, #1 @ 使用已保护的r4
pop {r4-r6, pc} @ 恢复寄存器并通过pc返回
4.3 函数调用场景
场景 1:汇编调用 C 函数
汇编调用 C 函数时,需按规则传递参数(R0~R3 传前 4 个参数,超过 4 个用栈),且用 BL 指令(保存返回地址到 LR)。
.global _start
_start:
/* 1. 设置栈(C函数需栈才能运行,栈地址需是4字节对齐) */
LDR SP, =0X80200000 @ 栈地址设为0X80200000(需是空闲内存区域)
/* 2. 传递参数(C函数 void C_LED_On(int pin, int status)) */
MOV R0, #3 @ R0 = pin(第一个参数)
MOV R1, #1 @ R1 = status(第二个参数)
/* 3. 调用C函数(BL保存LR,执行完返回当前处) */
BL C_LED_On
/* 4. 死循环(C函数返回后防止跑飞) */
Loop:
B Loop
场景 2:C 调用汇编函数
C 调用汇编函数时,需在汇编中用 .global 导出函数名,C 中用 extern 声明,参数传递规则同上。
/* 汇编文件:add.s */
.global Add @ 导出Add函数,供C调用
Add:
ADD R0, R0, R1 @ R0=输入a,R1=输入b,R0=结果(C中接收返回值)
BX LR
/* C文件:main.c */
extern int Add(int a, int b); // 声明汇编函数
int main() {
int res = Add(3, 5); // 调用汇编函数,res=8
return 0;
}
5. GUN核心指令详解
ARM 指令是汇编的 “执行单元”,Cortex-A7 常用指令按功能分类,以下为嵌入式开发高频指令(需重点掌握)。
5.1 数据传输指令(寄存器 / 特殊寄存器)
用于寄存器间、寄存器与特殊寄存器(CPSR/SPSR)的数据传递,核心指令 3 个:
| 指令 | 格式 | 作用 | 示例 |
|---|---|---|---|
| MOV | MOV Rd, Rn / #immed | 寄存器→寄存器,或立即数→寄存器(仅 16 位立即数) | MOV R0, R1(R0=R1)、MOV R0, #0X12 |
| MRS | MRS Rd, 特殊寄存器 | 特殊寄存器→通用寄存器(仅能读特殊寄存器) | MRS R0, CPSR(R0=CPSR,读处理器状态) |
| MSR | MSR 特殊寄存器,Rd | 通用寄存器→特殊寄存器(仅能写特殊寄存器) | MSR CPSR, R0(CPSR=R0,修改处理器状态) |
示例:
mov r0, #0x1234 @ 立即数→寄存器
mov r1, r0 @ 寄存器→寄存器
mrs r0, cpsr @ 特殊寄存器→通用寄存器
msr cpsr, r0 @ 通用寄存器→特殊寄存器
5.2 存储器访问指令(寄存器↔内存 / 外设)
ARM 不能直接访问内存 / 外设寄存器,需通过 LDR(读)和 STR(写)指令,支持按字节、半字、字(32 位)操作。
| 指令 | 格式 | 作用 | 示例(访问 I.MX6U GPIO1_GDIR) |
|---|---|---|---|
| LDR | LDR Rd, [Rn, #offset] | 内存(Rn+offset)→寄存器(字操作) | LDR R1, [R0](R1 = *R0,读地址值) |
| LDRB | LDRB Rd, [Rn, #offset] | 内存→寄存器(字节操作,8 位) | LDRB R1, [R0, #1](读 R0+1 地址的 1 字节) |
| LDRH | LDRH Rd, [Rn, #offset] | 内存→寄存器(半字操作,16 位) | LDRH R1, [R0](读 R0 地址的 2 字节) |
| STR | STR Rd, [Rn, #offset] | 寄存器→内存(字操作) | STR R1, [R0](*R0 = R1,写地址值) |
| STRB | STRB Rd, [Rn, #offset] | 寄存器→内存(字节操作) | STRB R1, [R0, #2](写 R0+2 地址 1 字节) |
| STRH | STRH Rd, [Rn, #offset] | 寄存器→内存(半字操作) | STRH R1, [R0](写 R0 地址 2 字节) |
实战场景:配置 GPIO 为输出并点亮 LED
.equ GPIO1_GDIR, 0X0209C004 @ GPIO1方向寄存器地址
.equ GPIO1_DR, 0X0209C000 @ GPIO1数据寄存器地址
LED_On:
/* 1. 配置GPIO为输出(GDIR对应位设1) */
LDR R0, =GPIO1_GDIR @ R0 = 寄存器地址
LDR R1, [R0] @ R1 = 当前GDIR值(读)
ORR R1, R1, #(1<<5) @ 第5位设1(配置引脚为输出)
STR R1, [R0] @ 写回GDIR(完成配置)
/* 2. 点亮LED(DR对应位设0,假设低电平点亮) */
LDR R0, =GPIO1_DR @ R0 = 数据寄存器地址
LDR R1, [R0] @ R1 = 当前DR值
BIC R1, R1, #(1<<5) @ 第5位设0(点亮)
STR R1, [R0] @ 写回DR
BX LR
5.3 压栈 / 出栈指令(现场保护)
用于批量保存 / 恢复寄存器,依赖栈指针(SP),Cortex-A 用 满递减栈(FD)(SP 指向最后入栈的元素,栈从高地址向低地址增长),核心指令 2 个:
| 指令 | 格式 | 作用 | 等价指令(FD 模式) |
|---|---|---|---|
| PUSH | PUSH | 寄存器列表入栈(批量保存) | STMFD SP!, |
| POP | POP | 栈数据恢复到寄存器列表(批量恢复) | LDMFD SP!, |
示例:
Interrupt_Handler:
push {r0-r3, r12, lr} @ 中断现场保护
mrs r0, spsr
push {r0}
/* 中断处理代码 */
pop {r0}
msr spsr, r0
pop {r0-r3, r12, pc} @ 直接恢复pc实现返回
5.4 跳转指令(程序流控制)
用于改变程序执行顺序,核心指令 3 个,区别在于 “是否保存返回地址” 和 “是否切换指令集”:
| 指令 | 格式 | 作用 | 适用场景 |
|---|---|---|---|
| B | B label | 直接跳转,不保存返回地址(单向跳转) | 跳 main 函数(不返回汇编)、死循环 |
| BL | BL label | 跳转前保存返回地址到 LR(可返回) | 调用函数(需返回当前处) |
| BX | BX Rm | 间接跳转(地址存 Rm),支持切换指令集 | 函数返回(BX LR)、跳不同指令集代码 |
示例:汇编初始化后跳 C main
.global _start
_start:
/* 1. 初始化栈(C函数运行依赖栈) */
LDR SP, =0X80200000 @ 栈地址需是空闲内存(如DDR中的地址)
/* 2. 初始化DDR(若代码在Flash,需将数据加载到DDR,此处省略) */
BL DDR_Init @ 调用汇编的DDR初始化函数(需返回)
/* 3. 跳C main函数(不返回,用B指令) */
B main @ 跳main,不再回到_start
6. 附录:常用指令速查表
| 指令类型 | 指令 | 功能简述 | 格式示例 |
|---|---|---|---|
| 数据传输 | MOV | 寄存器 / 立即数传输 | MOV R0, R1 |
| MRS | 特殊寄存器→通用寄存器 | MRS R0, CPSR | |
| MSR | 通用寄存器→特殊寄存器 | MSR CPSR, R0 | |
| 存储器访问 | LDR | 内存→寄存器(字) | LDR R1, [R0] |
| STR | 寄存器→内存(字) | STR R1, [R0] | |
| LDRB | 内存→寄存器(字节) | LDRB R1, [R0, #1] | |
| 压栈出栈 | PUSH | 寄存器入栈 | PUSH |
| POP | 栈→寄存器 | POP | |
| 跳转 | B | 直接跳转(不返回) | B main |
| BL | 跳转 + 保存 LR(可返回) | BL DDR_Init | |
| BX | 间接跳转(地址在寄存器) | BX LR | |
| 算术运算 | ADD | 加法 | ADD R0, R0, R1 |
| SUB | 减法 | SUB R0, R0, #1 | |
| 逻辑运算 | ORR | 按位或 | ORR R1, R1, #(1<<5) |
| BIC | 按位清除 | BIC R1, R1, #(1<<5) |

浙公网安备 33010602011771号