1.C语言基本知识
C语言之memcmp()函数(比较字符串的差值)
C/C++常用库函数表以及知识点
函数的可重入和不可重入
C语言中最常用标准库函数
C语言函数调用栈(一)
C语言函数调用栈(二)
C语言函数调用栈(三)
已释放的栈内存
C语言变量的存储布局
程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
函数调用时入栈顺序为
实参N~1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1~N
函数调用时的具体步骤如下:
1) 主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行时栈中。该操作会改变程序的栈指针。
注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。
2) 主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。
3) 若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。
4) 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。
5) 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。
6) 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。
7) 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。
8) 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。
9) 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。
步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。
(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。后续若还有其他函数调用,则其局部变量可能覆盖该栈区内容。常见情况有两种:前次调用影响当前调用的局部变量取值(函数的"遗产");被调函数返回指向栈内存的指针,主调函数通过该指针访问被调函数已释放的栈区内容(召唤亡灵)
C语言预处理命令详解
举例一些常用功能的宏定义,#与##,可变参数宏
在头文件中为了避免重复调用(如两个头文件互相包含对方),常采用这样的结构:
#ifndef <标识符>
#define <标识符>
//真正的内容,如函数声明之类
#endif
C语言字节对齐问题详解
通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。合理利用字节对齐还可以有效地节省存储空间。
如果没有进行字节对齐,有的变量值可能读取错误(float)
对齐准则
1) 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double(float)型为8字节。
2) 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。
3) 指定对齐值:#pragma pack (value)时的指定对齐值value。__attribute__((aligned(value)))
4) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
其中,有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N表示“对齐在N上”,即该数据的“存放起始地址%N=0”。而数据结构中的数据变量都是按定义的先后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体本身也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。
结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
更改对齐方式
主要是更改C编译器的缺省字节对齐方式。
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
- 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
- 使用伪指令#pragma pack(): 取消自定义字节对齐方式。
- 在编码时,可用#pragma pack动态修改对齐值。自定义对齐值后要用#pragma pack()来还原,否则会对后面的结构造成影响。
另外,还有如下的一种方式(GCC特有语法):
- __attribute((aligned (n))): 让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
- __attribute__ ((packed)): 取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
栈的对齐方式不受结构体成员对齐选项的影响。总是保持对齐且对齐在4字节边界上。
sscanf的字符串格式化用法
缓冲区溢出详解
可变参数函数详解
变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。
①_INTSIZEOF宏考虑到某些系统需要内存地址对齐。从宏名看应按照sizeof(int)即堆栈粒度对齐,即参数在内存中的地址均为sizeof(int)=4的倍数。例如,若在1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。
为便于理解,简化该宏为
#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x))
x = sizeof(int) - 1 = 3 = 0b’0000 0000 0000 0011
~x = 0b’1111 1111 1111 1100
一个数与(~x)相与的结果是sizeof(int)的倍数,即_INTSIZEOF(n)将n圆整为sizeof(int)的倍数。
②va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。
固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。
③va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。
va_arg宏的等效实现如下
//将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值
#define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))[-1]
④va_end宏使ap不再指向有效的内存地址。该宏的某些实现定义为((void*)0),编译时不会为其产生代码,调用与否并无区别。但某些实现中va_end宏用于函数返回前完成一些必要的清理工作:如va_start宏可能以某种方式修改堆栈,导致返回操作无法完成,va_end宏可将有关修改复原;又如va_start宏可能对参数列表动态分配内存以便于遍历va_list,va_end宏可释放此前动态分配的内存。因此,从使用va_start宏的函数中退出之前,必须调用一次va_end宏。
函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。
二维数组与高级指针
动态分配二维数组
C数组&结构体&联合体快速初始化
C语言头文件组织与包含原则
C语言表驱动法编程实践
2.标准文件函数(文件的打开、关闭、读和写等)
Linux 文件编程—fopen函数
C语言fgets()与fputs()详解
fgets()读到\n后结束(最后一个存的是\n)
3.C语言琐碎知识点
C语言宏定义中 ## 和#的作用
宏的高级使用--##,__VA_ARGS__, __FILE__, __FUNCTION__等
puts和printf区别
- \r,\n,\r\n的区别
Windows系统下txt文件放到Ubuntu系统中,\n变为\r\n,fread读出数据不同
环形缓冲区
4.字符串相关
C语言中将字符串转换为数字的方法
sscanf功能详解
sscanf()总结
char* test="100";
int addr = atoi(test);-------------------------------addr=100
char* p = "0xC10A37C0";
sscanf(p, "%x", &addr);-------------------------------addr=0xC10A37C0
char* pp="100";
sscanf(pp, "%d", &addr);-------------------------------addr=100