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
编译器视角
编译过程
编译器的作用:将高级语言转换为机器语言,使其可以运行 查看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 0int main(void){#if DEBUG == 1 printlog();#endif}gcc的CLI参数本身等价
#define <定义内容> 1extension: Linux 的feature宏 内核配置
#ifdef TOUCH_PANEL_DRIVERvoid 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 匿名unionenum枚举类型
给常量集合打包 形成新的数据类型 默认整型常量,若超出上限,编译器自行决定更大的数据类型 主动赋值/逐个+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空间有限 写入溢出越界 刚写入即时可能没问题 但越界部分不受保证,可能会被改变,尤其实际工程中容易出问题,隐秘难以察觉
指针
指针访问内存
标准数据类型
访问大小:指向的数据类型的大小 不管大端还是小端 指针都指向低地址侧
连续空间类型
结构体指针、数组指针
部分信息可能已经过时













