Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6Mobile wallpaper 7Mobile wallpaper 8
3921 字
20 分钟
进阶嵌入式C语言-学习笔记

Linux & Vim#

Skip

GDB#

usage:指定参数(CLI)、断点、分析crash、直接修改程序 gdb可以调试运行中的进程。gdb -p ID gdb可以调试crash后的程序,但需要一个core文件(dump) gdb调试编译好的程序。 前提是gcc编译时加上-g参数

GDB调试过程#

GDB打开elf文件,会出现gdb shell(?) 等待命令进行。 run/r:无断点直接运行 list:以某一行为中心上下展开查看代码 !clear(shell命令 jupyter notebook style) break:打断点,单步调试。 可以加具体行号/函数名 函数名则会停在函数入口。设置断点后run就会停在断点处 next:单布执行。 continue/c:继续执行

单步调试过程中:可以查看函数中变量信息。(打断点停在中途的时候) print: print 变量名 打印变量值 加上取地址运算符可取变量内存地址 set var 变量名 = 赋值 修改变量值 info: info <参数> 查看信息 参数为break可查看断点信息 参数为register可查看寄存器信息(汇编层盘查crash会用)thread 查看线程信息 delet: 删除断点 后跟编号 编号在info查 x: x < mem addr > 查看对应内存地址的值吗默认为HEX, 前面加上/d参数可以转换为十进制 thread: thread < 线程号 > 查看线程上下文 disass: 当前c代码对应汇编。 程序运行时pc指针指向part

编译器视角#

编译过程#

编译器的作用:将高级语言转换为机器语言,使其可以运行 查看gcc编译过程: 编译过程中加上参数-v,可以查看编译过程中使用的大部分参数、过程。 e.g. step1 编译:输入.c文件,调用cc1(c compiler 1) output.s结尾文件(汇编文件) step2 汇编: as命令 输入.s文件 输出.o(.obj)文件(目标文件) step3 链接: collect2 输入.o文件,与系统内obj进行链接,使用系统的库,输出bin(二进制ELF可执行文件)

细分编译器工作过程#

编译#

预编译(cc1)#

参数 -E 生成.i结尾的预编译文件,本质仍然还是文本,包含了各种字符和头文件。 主要行为:包含头文件(#include) 宏展开(#define) include:包含入头文件 define:内容替换 Q:包含.c文件会在预编译中怎么处理? A:也会正常处理和宏展开,但一般而言不这么用(包含的是.h,.c为代码实现)

汇编(cc1)#

参数-S 生成.s结尾的汇编文件,本质还是文本,但已经转化为汇编语言。 汇编指令 是与处理器指令集强相关的,处理器指令集不同生成的汇编指令不同。

链接(as 命令)#

参数-c生成.o结尾的链接文件,贴近机器码已几乎不可读。 可输入.s文件生成.o文件。

.o文件也可生成elf文件

review:链接的过程是找并包含进需要的库。 所以,如果工程中有很多c文件,且只是修改了某一个文件,这时候只需要将这个文件的.o文件重新生成就可以生成新的可执行程序,速度会更快

格式转换#

elf格式由操作系统处理后转化为指令进行,单片机是裸机开发,有一个转换成hex的格式转换过程。比如stm32开发过程中有个fromelf的组件会进行这一工作。linux中也有readelf对binary文件进行解析

常见编译错误#

预编译错误#

1.头文件找不到。可能是文件不存在或者是未引入相关目录。避免使用相对路径包含头文件,而是在编译过程中加CLI参数。 gcc的CLI参数 -I < 目录 > 预编译阶段是不检查语法错误的。 2.条件编译不成对 预处理阶段还有一种判断条件 “#ifdef” “#elif” “#else” “#endif”要有end结束

编译错误#

1.缺失结束符 …

链接错误#

函数/变量未定义/重复定义

条件编译#

release版本要进行信息脱敏,性能保证,去掉调试信息

可以

#ifdef DEBUG
printlog()
#endif

前面宏定义#define DEBUG 就可以运行这段内容

若不能改代码,gcc可加CLI参数 -D < 定义内容 >来外加宏定义。 也可以宏定义数值进行判断

#define DEBUG 0
int main(void){
#if DEBUG == 1
printlog();
#endif
}

gcc的CLI参数本身等价

#define <定义内容> 1

extension: Linux 的feature宏 内核配置

#ifdef TOUCH_PANEL_DRIVER
void tp_driver(void){
...
}
#endif
int main(void){
#ifdef TOUCH_PANEL_DRIVER
tp_driver();
#endif
}
#ifndef XXX
#warning "内容" //如果没有定义XXX会在编译过程中进行警告
#error "内容" //...就会报错
#endif

更多的宏定义#

基本格式

#define 宏名 宏体

宏是纯粹的文本替换,不在乎语法。目的在于增强代码的可读性(替换代码里不明意义的神秘数字),使它们的意义明确。 目的:让编译器多干活,让读的人更轻松 赋值过程中若有计算 编译器会主动进行计算并存入

宏函数#

文本展开-用空间换性能 要求:不要太复杂 定义方法

#define 函数名(参数) 基于参数的函数体

但仍然是简单的文字替换 最好加个小括号保护起来 写多条 加括号 加分号 但是 if语句也要注意 可能会导致多条语句只执行第一条 记住是完全的文字替换就可以了 放else里用可以给宏函数包一个do while(0)

调试log宏#

调用对应函数 ,# 实现字符化,认作字符串进行替换

#define LOG_INFO(x) printf("%s is %d \n",#x,(x))

两个# 实现字符串连接

#define DAY_DECLARE(x) int day##x
#define DAY(x) day##x
int main(void){
DAY_DECLARE(1) = 100
int x = 1;
printf("%d",DAY(x));
}

输出为100 内存中: day1 = 100

__ FUNCTION __ 输出当前所在函数的函数名 __ LINE __ 输出当前行号 __ FILE __ 输出当前文件名

关键字#

sizeof()#

sizeof(常量 变量 数据类型) 注意:sizeof不是函数 函数不能传入数据类型作为实参 传入参数即使是函数也不会执行 计算动作在编译过程中完成

char/uint8_t#

0-255 8bit/1byte

int#

大小取决于CPU数据总线大小。可能是2byte/4byte/8byte(16/32/64位机)

short#

2byte

long#

8 byte

double/float#

float#

4byte 单精度浮点

double#

8byte 双精度浮点

(un)signed#

(无)有符号

void#

void表示一个东西没有值 函数无返回值/参数 解决编译器警告 解决变量未使用的情况 void* 表示数据空间

自定义类型#

struct结构体#

内存累加对齐 机制:对齐到4(32位) 取决于数据总线 自动填空 但是如果一次能读多个完整的也可能不留空 比如两个char连续存储 内存上相邻但一次仍然读四个字节

小知识:x86会自动处理非对齐 但是硬件效率会降低 ARM是部分支持 群指令不支持

union 联合体#

所有成员共享一块内存。以最大的成员为尺寸 然后从第一块内存开始按顺序储存修改内容 占位小的变量存在high位/low位取决于平台为小端/大端 但不管大端小端 起始地址都是低地址

struct + union 配合#

构造数据包 修改封闭拓展开放 __ attribute __ 可以取消对齐

#pragma anon_union 匿名union
enum枚举类型#

给常量集合打包 形成新的数据类型 默认整型常量,若超出上限,编译器自行决定更大的数据类型 主动赋值/逐个+1 不赋值就会+1

地址/指针类型#

指向变量所在地址 直接访问内存

typedef#

为数据类型起别名 方便多处使用 一般命名加_t

修饰关键字#

auto#

默认缺省

register#

放变量进寄存器 建议性 寄存器很小CPU不一定会存 且修饰后的变量无法取地址

static#
规定变量内存从静态全局内存区分配#

函数栈内的变量用后即毁 static修饰可使其生命周期覆盖整个程序执行过程 也可以定义全局变量跨文件引用(extend关键字也无法使用)

限定函数和变量作用域#

可以将函数限定在一个文件里使用,避免重复定义

extern#

声明没有被static修饰的变量 架构设计中尽量不要用

const#

恒定不变 只读变量 也能改 但别改 采用就近原则

const int * a //*a不能变
int const * b //*d内容不能变
int * const c //c指向地址不能变
const int * const d //d指向地址和d的内容都不能变

const就是为了替代宏的

volatile#

表示变量可能在程序控制范围之外发生变化,对其取消编译器优化 例如:UART写内存

逻辑关键字#

if else#

skip

switch#

switch(表达式/整型) case: 表达式值 后接break不然会落下去 default:无法匹配默认进行

do while for#

skip

continue/break/return#

skip

goto#
label:
printf("come here!");
goto label;

函数视角#

封装逻辑实现功能 避免重复造轮子

三大属性#

函数名/地址 输入 返回值

传参#

传参的本质:内存拷贝#

int func(int a);//a是形参
int main(){
int x =114;
int func(x)//x是实参
}

传参会把实参的值拷贝到形参所在的内存。

值传递#

一般传入函数的参数,标定数据类型非地址的传入的都是数值,不会影响原有地址内容(开辟了一块新的栈空间进行操作),可以对数据进行隔离和保护。 恢复栈中的局部变量:汇编反推位置

地址传递#

形参当作返回值#

向函数传入地址(指针),借传入的形参将值传到函数之外

连续空间传递#

传数组的时候是传多个内存空间。传连续空间传低地址 对非字符空间 需传入连续地址的长度,否则默认返回结果

C与面向对象#

继承#

继承:使用已存在的类建立新的类,新的类可以增加数据类型和方法,也可以使用父类的功能,但不能选择性继承。利于复用代码

class shape {
protected:
int width;
int height;
public:
void setWidth(int w){
width = w;
}
void setHeight(int h){
width = h;
}
}
class rect : public shape {
public:
int getArea() {
return(width * height);
}
}

rect继承了shape的width、height和设置函数

封装#

将抽象的数据和行为/方法结合,形成一个有机的整体。增强安全性和简化编程,使用者只需要调用接口就可以使用数据成员

class car{
protected:
int direction;
int brake;
int throttle;
public:
void setDirection;
void brakeControl;
}

c中,可使用函数指针指向对应函数

struct person{
char name[8];
int age;
void (*getname)(struct person *p,char *name);
void (*getage)(struct person *p, int age);
};
static void getname(struct person *p,char *name){
strcpy(name,p->name);
}

实现数据和操作的结合 各自单一职责

多态#

通过父类的指针调用子类。 作用:屏蔽差异,调用父类方法,储存到子类实现

class person{
protected:
char name[24];
int age;
public:
virtual void getname(char *n){
strcpy(n,name);
}
person(const char *n, int a){
strcpy(name,n);
age = a;
}
}
class usa_person public person {
public:
void getname(char *n){
sprintf(n,"%s",name);
sprintf(n+strlen(n),"%s","-usa");
}
usa_person(const char *n,int a) :person(n,a){}
}

复用person类的行为到usa_person 后面也可以有china_person来给它加后缀 但不会冲突 避免差异影响

struct person{
char name[24];
int age;
void (*getname)(struct person *p,char *name);
}
struct usa_person{
struct person p;
void (*getname)(struct person *p,char *name);
}

重载#

声明几个参数不同的名称相同的函数

可变参数函数#

va_list = struct __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
}
#define va_start(v,l) __builtin_va_start(v,l)
#define va_end(v) __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)
static void va_func(int n,...){
va_list ptr;
va_start(ptr,n);
while(n--){
printf("%d \n",va_arg(ptr,int));
}
va_end(ptr);
}
int main(void) {
va_func(1,100);
va_func(2,200,300);
}//遍历可变参数列表

回调函数+void*#

cpp里重载,接口归一化

c中实现

void exchange_int(void *a, void *b){
int *_a = (int*)a;
int *_b = (int*)b;
int tmp = 0;
tmp = *_a;
*_a = *_b;
*_b = tmp;
}
void exchange_double(void *a, void *b){
double *_a = (int*)a;
double *_b = (int*)b;
double tmp = 0;
tmp = *_a;
*_a = *_b;
*_b = tmp;
}
typedef void (*func)(void* ,void*);
void swap(func exchange,void *a,void *b){
exchange(a,b);
};

规定函数,向swap传入函数名,回调实现参数可变,接口归一化(swap)

弱连接函数#

几个定义相同,编译过程中覆盖掉weak属性的函数,实现override效果 属性:weak

<1.c>
void __attribute__((weak)) config(void){
//...
//...
//...
LOG(config1)
};
<2.c>
void config(void){
//...
//...
//...
LOG(config2);
}
int main(){
config();
}

两文件联合编译,这种情况有weak属性的会被另一个同名函数覆盖掉。

solid设计原则#

单一职责#

一个类只负责一件事,保证逻辑清晰,职责分明。

高类聚 低耦合

C语言,则是结构体设计优化

开闭原则#

拓展开放 修改封闭 模块化设计:修改不变 拓展开放

编译器属性:section

施工中…

里氏替换原则#

子类可以拓展父类功能 但不能改变父类原有功能。继承其实某种程度上破坏了封装,里氏替换原则是为了保护父类的封装性

class birds:{
protected:
int speed;
int time;
public:
void SetSpeed();
void GetSpeed();
...
}
class swallow : public birds{
}
class ostrich : public birds {
public:
void SetSpeed(int s){
speed = 0;
}
}//影响了父类的动作功能,违反里氏替换原则

父类子类不同 子类基于父类 子类不能比父类少,子类只能从父类拓展

接口隔离规则#

不能强迫用户端实现一个它用不上的接口,避免多余设计

依赖倒置原则#

高层次模块不得依赖低层次模块。 e.g.业务逻辑不能依靠/直接调用驱动层。 外界的改变不能影响底层逻辑

内存视角#

基本概念#

内存分为静态区和动态区

静态内存#

编译决定分配。

内容权限
代码段R
只读数据(const)R
全局数据初始化段RW
全局数据未初始化段RW
代码段#

可以访问可以读内容,但试图修改会fault

只读数据#

只读数据还包括直接存储的字符串。

全局变量#

static修饰 初始化的:DATA 没初始化的

动态内存#

运行时分配。

内容权限
堆空间(向上生长)RW
栈空间(向下生长)RW
堆空间#

malloc/free的内存空间 使用指针访问时,指针不能为空。 malloc与free要成对以避免内存泄漏

堆空间有限 可能会分配失败 生命周期自定义

栈空间#

函数运行时上下文分配的空间。

生命周期为所在函数运行开始到结束

内存溢出#

栈溢出#

栈被消耗完: 递归没有正常退出,消耗完空间; 超级大的局部变量(超大数组之类,字库这一块

栈缓冲区溢出#

例:strcpy,长内容拷贝入短区域造成越界访问 所以一般用strncpy 规定传入长度

堆缓冲区溢出#

malloc空间有限 写入溢出越界 刚写入即时可能没问题 但越界部分不受保证,可能会被改变,尤其实际工程中容易出问题,隐秘难以察觉

指针#

指针访问内存#

标准数据类型#

访问大小:指向的数据类型的大小 不管大端还是小端 指针都指向低地址侧

连续空间类型#

结构体指针、数组指针

进阶嵌入式C语言-学习笔记
https://di404.uk.eu.org/posts/senior-embedded-c-note/
作者
Reconi(Rino)
发布于
2025-10-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00