预备知识
- 本地调试,交叉调试
- 本地调试gcc,有执行文件之后直接
./hello_x86
- 交叉调试,在arm架构上调试,有执行文件后通过模拟器执行
qemu-arm -L /usr/arm-linux-gnueabihf ./hello_arm
- 本地调试gcc,有执行文件之后直接
- 交叉编译工具
- 这里采用通用的ARM 32位交叉编译器
bash
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf
#验证:
arm-linux-gnueabihf-gcc -v
- 有时候不确定架构,先采用gcc本地进行调试,最后进行模拟器执行或者硬件编译
进行第一个c文件进行编写
terminal中进行编写
- 创建一个文件夹
mkdir code
- 编写一个c文件
nano hello.c
- 编写完成后: Ctrl + X退出 | Y | Enter 确认文件名
hello.c
#include<stdio.h>
int main(void){
printf("hello world\n");
return 0;
}
- 对文件进行编译(arm_32架构下)
-
分步操作
arm-linux-gnueabihf-gcc -E hello.c -o hello.i
预处理文件 -E: 告诉编译器,只执行预处理阶段 <stdio.h> 文件的所有内容都被展开并包含进来arm-linux-gnueabihf-gcc -S hello.i -o hello.s
生成汇编文件 -S: 告诉编译器,执行到编译阶段arm-linux-gnueabihf-gcc -c hello.s -o hello.o
生成目标文件 汇编代码转换成机器可以执行的、但尚未链接的二进制目标代码arm-linux-gnueabihf-gcc hello.o -o hello_arm
链接生成最终的可执行文件 将我们的 hello.o 文件与它所需要的C标准库函数链接在一起,生成一个完整的、可以在ARM Linux系统上运行的程序
-
一步执行操作从c文件到链接文件,两种链接方式
- 动态链接方式:
arm-linux-gnueabihf-gcc hello.c -o hello_arm
(模拟器测试常用) - 静态链接方式:
arm-linux-gnueabihf-gcc -static hello.c -o hello_arm_static
- 动态链接方式:
-
- 使用QEMU模拟器运行 进行快速测试
- 两种用户模拟器
- 静态模拟器:
sudo apt install qemu-user-static
- 所有需要的库代码都包含进去:
arm-linux-gnueabihf-gcc -static hello.c -o hello_arm_static
然后执行qemu-arm-static ./hello_arm_static
- 所有需要的库代码都包含进去:
- 动态模拟器:
sudo apt install qemu-user
- 要告诉QEMU去哪里寻找ARM的库文件:
arm-linux-gnueabihf-gcc hello.c -o hello_arm
然后执行qemu-arm -L /usr/arm-linux-gnueabihf ./hello_arm
-L 参数为QEMU指明了ARM程序所需的动态库的位置
- 要告诉QEMU去哪里寻找ARM的库文件:
- 静态模拟器:
- 两种用户模拟器
Makefile介绍
引入
当创建的多文件项目时main.c, math_functions.c, math_functions.h
需要按顺序编译
bash
# 1. 编译 main.c
arm-linux-gnueabihf-gcc -c main.c -o main.o
# 2. 编译 math_functions.c
arm-linux-gnueabihf-gcc -c math_functions.c -o math_functions.o
# 3. 链接所有 .o 文件
arm-linux-gnueabihf-gcc main.o math_functions.o -o my_app_arm
如果项目有几十个文件,只修改了 math_functions.c,理论上只需要重新执行第2步和第3步,但很容易忘记或搞混。
Makefile 的使命就是将这个过程自动化、精确化。只需要定义好规则,剩下的交给 make 命令去做。
使用
Makefile 核心概念,格式
markdown
目标 (Target): 依赖1 (Dependency1) 依赖2 (Dependency2) ...
<Tab>命令 (Command)
makefile文件编写
基础清晰的依赖:
- make 命令默认执行第一个目标 my_app_arm。
- make 发现 my_app_arm 依赖 main.o 和 math_functions.o。
- 它会先去寻找生成 main.o 的规则,发现 main.o 依赖 main.c 和 math_functions.h。如果 main.c 或 math_functions.h 更新了,它就会执行 gcc -c 命令来重新生成 main.o。
- math_functions.o 的处理过程同上。
- 当所有 .o 文件都准备好且是最新版本后,make 才最后执行生成 my_app_arm 的链接命令。
Makefile
my_app_arm: main.o math_functions.o
arm-linux-gnueabihf-gcc main.o math_functions.o -o my_app_arm
main.o: main.c math_functions.h
arm-linux-gnueabihf-gcc -c main.c -o main.o
math_functions.o: math_functions.c math_functions.h
arm-linux-gnueabihf-gcc -c math_functions.c -o math_functions.o
引入变量,让 Makefile 更易维护:
Makefile
# --- 变量 ---
CC = arm-linux-gnueabihf-gcc
TARGET = my_app_arm
OBJS = main.o math_functions.o
# --- 规则 ---
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
main.o: main.c math_functions.h
$(CC) -c main.c -o main.o
math_functions.o: math_functions.c math_functions.h
$(CC) -c math_functions.c -o math_functions.o
添加"清理"规则和伪目标:
Makefile
# --- 变量 ---
CC = arm-linux-gnueabihf-gcc
TARGET = my_app_arm
OBJS = main.o math_functions.o
# --- 规则 ---
# 添加一个默认的 all 目标 就可以使用make或这make all编译整个项目
#如果没有定义只能使用make编译整个项目
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
main.o: main.c math_functions.h
$(CC) -c main.c -o main.o
math_functions.o: math_functions.c math_functions.h
$(CC) -c math_functions.c -o math_functions.o
# --- 清理规则 ---
.PHONY: clean # 声明 clean 是一个"伪目标"
clean:
rm -f $(TARGET) $(OBJS)
使用模式规则和自动化变量:
%.o: %.c: %
是一个通配符,对于任何一个 .o 文件,它都依赖于一个同名的 .c 文件。- 自动化变量 (Automatic Variables):
$@
: 代表规则中的"目标"。在%.o: %.c
规则里,它就是那个 .o 文件(如 main.o)。$<
: 代表规则中的"第一个依赖"。在%.o: %.c
规则里,它就是那个 .c 文件(如 main.c)。$^
: 代表规则中的"所有依赖"。在$(TARGET): $(OBJS)
规则里,它就是 main.o math_functions.o。
- 无论 OBJS 变量里有多少个 .o 文件,那条通用的 %.o: %.c 规则都能自动处理它们的编译。
Makefile
# --- 变量 ---
CC = arm-linux-gnueabihf-gcc #也可以换成gcc编译
TARGET = my_app_arm
OBJS = main.o math_functions.o
# --- 规则 ---
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
# 模式规则
%.o: %.c
$(CC) -c $< -o $@
# --- 清理规则 ---
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
makefile使用
- 默认编译
make
它会按照特定的顺序在当前目录下寻找一个默认的 Makefile 文件名make all
编译 文件中定义的目标make clean
清理项目
- 指定编译
make -f Makefile-V1
-指定编译makefile文件make -f Makefile-V1 clean
ARM汇编
指令
mov 不访问内存
- 将一个立即数(一个固定的数值)或另一个寄存器的值,复制到目标寄存器。
- C语言等效操作: a = 10; 或 a = b;
asm
MOV R0, #10 @ 将立即数10放入寄存器R0。'#'表示这是一个立即数。
MOV R1, R0 @ 将寄存器R0的值复制到寄存器R1。
ldr
- 从内存 (Memory) 中加载 (Load) 数据到寄存器。读内存的唯一方法。
- C语言等效操作: int a = *p;
- 在 C 里只有在定义/声明指针变量时才是"声明指针"的符号; 其余场景(表达式里)它都表示解引用(dereference)
asm
@ 假设R1中存放了一个内存地址
LDR R0, [R1] @ 读取R1所指向的内存地址中的数据,并存入R0。'[]'表示内存访问
@ 也可以使用偏移量
LDR R0, [R1, #4] @ 读取 (R1指向的地址 + 4字节) 处的数据,存入R0。常用于访问结构体成员或数组元素。
@ 也可以使用读取相加
ldr R0,[R1],#4 @读取R1到R0然后R1=R1+4
str
- 将寄存器中的数据存储 (Store) 到内存。这是写内存的唯一方法。
- C语言等效操作: *p = a;
asm
@ 假设R1中存放了一个内存地址,R0中存放了要写入的数据
STR R0, [R1] @ 将R0中的数据,写入到R1所指向的内存地址。
@ 同样可以使用偏移量
STR R0, [R1, #4] @ 将R0中的数据,写入到 (R1指向的地址 + 4字节) 处。
@ 也可以使用相加存储
str r0,[r1],#4 @将r0中的数据保存到地址为r1的内存单元中然后r1=r1+4
add
C语言等效操作: c = a + b;
asm
@ 假设 a 在 R1, b 在 R2
ADD R0, R1, R2 @ R0 = R1 + R2
sub
C语言等效操作: c = a - b;
asm
@ 假设 a 在 R1, b 在 R2
SUB R0, R1, R2 @ R0 = R1 - R2
mul
C语言等效操作: c = a * b;
asm
@ 假设 a 在 R1, b 在 R2
MUL R0, R1, R2 @ R0 = R1 * R2
逻辑运算-位操作

比较与跳转指令
CMP:C语言等效操作: if (a == b) 中的 a == b 这部分
asm
CMP R0, R1 @ 比较R0和R1的值,并设置标志位
C语言等效操作: goto 或 if/else 等流程控制
marodown
B label: 无条件跳转。
BEQ label: Equal,如果相等则跳转 (Z标志位为1)。
BNE label: Not Equal,如果不相等则跳转 (Z标志位为0)。
BGT label: Greater Than,如果大于则跳转。
BLT label: Less Than,如果小于则跳转。
BGE label: Greater than or Equal,大于等于。
BLE label: Less than or Equal,小于等于。
asm
CMP R0, R1 @ 比较 R0 和 R1
BNE skip_if_body @ 如果不相等(NE),就跳转到 skip_if_body 标签
MOV R2, #1 @ 如果相等,就执行这句,将1赋给R2
skip_if_body:
@ ... 后续代码
函数调用指令BL (Branch with Link)
C语言等效操作: my_function(a, b);
asm
BL my_function_label @ 调用 my_function_label 处的函数
@将下一条指令的地址(即返回地址)保存到链接寄存器 LR (R14)。然后跳转到目标函数的标签处。
AAPCS
ARM Architecture Procedure Call Standard:ARM 架构过程调用标准
寄存器说明:
调用者,被调用者:
- 调用者保存 (Caller-Saved)
- 调用者想保住手里的值 → 自己(caller)压栈。
- 被调用者保存 (Callee-Saved)
- 被调用者想借用别人的寄存器 → 自己(callee)压栈、恢复。
函数调用 - 参数传递:
- 规则1:前四个参数
- 函数的前4个整型或指针参数,依次通过寄存器 R0, R1, R2, R3 传递
C
void set_led_color(int led_id, int red, int green, int blue);
// 调用它
set_led_color(3, 255, 128, 0);
asm
MOV R0, #3 @ 第1个参数 led_id
MOV R1, #255 @ 第2个参数 red
MOV R2, #128 @ 第3个参数 green
MOV R3, #0 @ 第4个参数 blue
BL set_led_color @ 发起调用
- 规则2:第五个及以上的参数
- 从第5个参数开始,所有额外的参数都通过堆栈 (Stack) 传递。参数入栈的顺序是从右到左。
C
void draw_rect(int x, int y, int w, int h, int color,int a);
// 调用
draw_rect(10, 20, 100, 50, 0xFF0000,0); // 6个参数
asm
MOV R0, #10 @ 准备第1个参数 x
MOV R1, #20 @ 准备第2个参数 y
MOV R2, #100 @ 准备第3个参数 w
MOV R3, #50 @ 准备第4个参数 h
PUSH {R5,R6} @(可选)先保存可能被覆盖的寄存器后续,POP {R5, R6} 还原现场,与前面的 PUSH 成对
MOV R5,#0xFF0000
MOV R6,#0
PUSH {R6,R5} @ 将第5,6个参数压入堆栈 (从右到左,所以它先入栈)
BL draw_rect @ 发起调用
ADD SP, SP, #8 @ 调用结束后,清理堆栈 (将刚才压入的8字节参数弹出)
只是把栈指针 抬高 8 字节,它们仍在内存里,但已不在当前栈帧范围内,后续被别的 push 覆盖即可
- 函数调用 - 返回值
- 对于不超过32位的整型或指针,返回值必须放在寄存器 R0 中。对于64位的值(如 long long),则同时使用 R0 和 R1。
C
int add(int a, int b) {
return a + b;
}
int result = add(5, 7);
asm
add:
@ 根据AAPCS,参数a在R0,b在R1
ADD R0, R0, R1 @ 计算 R0 + R1,结果存回 R0
MOV PC, LR @ 返回。此时R0里已经放好了返回值
main:
MOV R0, #5
MOV R1, #7
BL add
@ add函数返回后,R0中就是结果12
MOV R4, R0 @ 将结果从R0保存到R4 (一个被调用者保存寄存器)
- 堆栈 (Stack) 与栈帧 (Stack Frame)
- 堆栈在一个函数运行期间,主要用于存放:
- 传递超过4个的参数。
- 保存局部变量。
- 保存"被调用者保存"的寄存器(R4-R11)。
- 保存返回地址 LR (如果当前函数还需要调用其他函数)。
- 堆栈在一个函数运行期间,主要用于存放:
asm
my_complex_function:
@ 函数序言 (Prologue) - 建立栈帧
PUSH {R4, R5, LR} @ 1. 保存需要用到的"被调用者保存"寄存器(R4,R5)和返回地址(LR)
SUB SP, SP, #8 @ 2. 在堆栈上为2个局部变量预留8字节空间
@ ... 函数主体代码 ...
@ 可以自由使用 R0-R3, R4, R5
@ 函数尾声 (Epilogue) - 销毁栈帧
ADD SP, SP, #8 @ 1. 释放局部变量空间,把栈指针抬高 8 字节,等于告诉 CPU:这块内存已归还,随时可被新数据覆盖。
POP {R4, R5, PC} @ 2. 恢复保存的寄存器,并将保存的LR值直接弹入PC,实现一步返回!