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,实现一步返回!
相关推荐
溯光笔记2 小时前
服务器内网穿透NPS搭建过程 - 服务端linux服务器 客户端windows系统 - 溯光笔记
linux·服务器·windows
2501_930124702 小时前
Linux之Shell编程(三)流程控制
linux·前端·chrome
Sadsvit4 小时前
Ansible 自动化运维工具:介绍与完整部署(RHEL 9)
linux·运维·centos·自动化·ansible
偶像你挑的噻4 小时前
linux应用开发-环境构建
linux
好名字更能让你们记住我6 小时前
Linux网络基础1(一)之计算机网络背景
linux·服务器·网络·windows·计算机网络·算法·centos
M1A16 小时前
Linux:数字世界的隐形基石与开源革命的王者
linux·后端·操作系统
青草地溪水旁7 小时前
VMware 设置 Ubuntu 虚拟机桥接模式完整教程
linux·ubuntu·桥接模式
淮北4947 小时前
linux系统学习(4.常用命令)
linux·运维·学习
Prejudices7 小时前
Linux查看有线网卡和无线网卡详解
linux·网络