linux应用开发-嵌入编程基础

预备知识

  • 本地调试,交叉调试
    • 本地调试gcc,有执行文件之后直接./hello_x86
    • 交叉调试,在arm架构上调试,有执行文件后通过模拟器执行qemu-arm -L /usr/arm-linux-gnueabihf ./hello_arm
  • 交叉编译工具
    • 这里采用通用的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程序所需的动态库的位置

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,实现一步返回!
相关推荐
阿部多瑞 ABU30 分钟前
`chenmo` —— 可编程元叙事引擎 V2.3+
linux·人工智能·python·ai写作
徐同保1 小时前
nginx转发,指向一个可以正常访问的网站
linux·服务器·nginx
HIT_Weston1 小时前
95、【Ubuntu】【Hugo】搭建私人博客:_default&partials
linux·运维·ubuntu
实心儿儿2 小时前
Linux —— 基础开发工具5
linux·运维·算法
oMcLin2 小时前
如何在SUSE Linux Enterprise Server 15 SP4上通过配置并优化ZFS存储池,提升文件存储与数据备份的效率?
java·linux·运维
王阿巴和王咕噜6 小时前
【WSL】安装并配置适用于Linux的Windows子系统(WSL)
linux·运维·windows
布史6 小时前
Tailscale虚拟私有网络指南
linux·网络
水天需0106 小时前
shift 命令详解
linux
wdfk_prog6 小时前
[Linux]学习笔记系列 -- 内核支持与数据
linux·笔记·学习
Xの哲學7 小时前
深入剖析Linux文件系统数据结构实现机制
linux·运维·网络·数据结构·算法