一、C标准库
1. stdio.h
1.1. 字符串格式化输出函数 snprintf()
函数原型
1 |
|
- snprintf是指定长度的格式化输出
- 返回值为format拼接的长度,不是被写入后字符串的长度
- 其中的
__maxlen
可以指定为sizeof(__s)
,snprintf只会写入__maxlen - 1
的字符,并在最后拼接\0
1.2. 文件操作
1) 函数原型
1 | FILE *fopen (const char *__restrict __filename, const char *__restrict __modes); |
2. string.h
2.1. 输出家族函数
1 |
|
以下函数功能与上面的一一对应相同,只是在函数调用时,把上面的…对应的一个个变量用va_list
调用所替代。在函数调用前ap要通过va_start()
宏来动态获取。
1 |
|
format格式说明
1 | //%s |
2.2. 字符串拷贝 strncpy()
函数原型
1 |
|
- strncpy拷贝字符串,会检测
__src
里面的结束符,只拷贝到结束符 - 如果
__src
长度大于__n
,将拷贝__n
个字符到__dest
,不会赋值结尾\0
,需要手动赋值 - 如果
__src
长度小于__n
,将拷贝strlen(__src)
个字符到__dest
,剩余空间赋值\0
2.3. 初始化函数 memset()
1 |
|
函数是按byte(8 bits)进行初始化的,每个字节均会被初始化为__c
取前八位的值。
2.4. 字符串拷贝 strdup()
1 |
|
2.5. 字符串比较函数 strcmp和strncpm
1 |
|
3. setjmp
3.1. 非局部跳转函数 setjmp()/longjmp()
1 |
|
实例
1 |
|
输出
1 | Setjmp code 0 |
注意事项
- 在上述实例中发现类没有被析构,这两个函数可以实现跳转但是不会检测类相关,所以类不会被析构
4. dlfcn.h 动态链接库
4.1. dlopen()/dlsym()/dlclose() 打开/加载符号/关闭 动态链接库
示例用法
1 | int main() { |
4.2. 加载下一个动态库符号
- 动态链接库符号查找是按照加载顺序查找的,比如有两个动态链接库有同一个
init()
函数,哪个库先加载,调用就会使用哪个库中的函数。 - 如果想要使用下一个动态库中的符号,可以使用
RTLD_NEXT
作为handle
传入 - 使用
RTLD_NEXT
需要加编译选项-D_GNU_SOURCE
示例
- 使用
LD_PRELOAD=libtest.so xxx
将二进制中调用动态库的函数log_init()
hook掉,做一些自己的操作 - 完成自己逻辑后,调回原来的函数
1 | /** |
5. time.h 时间处理函数
5.1. tm指针释放的问题
- tm指针在内部是一个固定的内存,每次调用都会修改此内存的值,外部不用释放
- 但是需要考虑多线程和多次调用的问题,调用之后前一次结果就无效了
二、语法和类型
1. 32位和64位大小区别
short
相当于short int
long
相当于long int
size_t
相当于unsigned long
long
和指针大小都等于编译环境的位数
1 | int main(int argC, char* arg[]) { |
2. char *a
和char a[]
的区别
1 | char *a = "Hello"; |
char *a = "Hello"; | char a[] = "Hello"; | |
---|---|---|
字符串所在区域 | 常量区 | 栈 |
可读可写 | 常量区不可写 | 可读可写 |
赋值时刻 | 编译时确定 | 运行时确定 |
存取效率 | 属于静态存储区,较慢 | 存于栈上,较快 |
sizeof(a) | 指针的大小,取决于编译环境 | 5 + 1,字符串长度加'\0' |
3. const的使用
1 | const char *p; // *p是const,p可变:const 后面紧跟的是char,所以*p是一个char字符,不可变 |
4. struct声明
4.1. 定义位变量
1 | struct test_t { |
- 针对小端模式
- 定义在前面的是低位,即a是16位中的0位
- 但是小端模式下,低位在低地址,所以修改了a为1,则得到的是内存分布为
0x01
、0x00
,但uint16_t打印还是0x0001
5. 宏定义
5.1. #if
宏
1 | /********** ifdef形式 **********/ |
5.2. 预定义宏
1) 平台相关
(1) windows
WIN32
: 由头文件minwindef.h
定义,一般是判断是否有调用windowsapi_WIN32
: 32位和64位程序都有,由编译器指定_WIN64
: 只有64位程序才有,由编译器指定
(2) linux
- gcc定义了下面的几个宏
1 | => gcc -dM -E - < /dev/null | grep -i linux |
- clang定义
1 | => clang -dM -E -x c /dev/null | grep -i linux |
通用写法
1 |
|
2) 编译相关
(1) __stdcall
/__cdecl
/__fastcall
/__pascal
__stdcall | __cdecl | __fastcall | __pascal | |
---|---|---|---|---|
参数压栈顺序 | 从右到左 | 从右到左 | 左边两个大小不大于4个字节(DWORD)的参数放在ECX和EDX寄存器,其余的参数从右到左 | 从左到右 |
栈清理方 | 被调用的函数 | 调用函数 | 被调用的函数 | 被调用的函数 |
函数名 | 前缀_ ,名称后跟@ 和参数列表字节数的十进制int func(int a, double b) => _func@12 | 前缀_ | 前缀@ ,名称后跟@ 和参数列表字节数的十进制int func(int a, double b) => @func@12 | 函数名全部大写 |
备注 | winapi使用,一般自清理堆栈是减少调用者大小 | C/C++默认调用方式,由于外部清理,一般作为可变参数函数使用 | 前两个参数放入寄存器,更快调用,一般给参数很少的函数使用 | 使用__stdcall 代替 |
6. static和inline
- static代表声明函数作用域,函数声明成static,将会仅在一个c文件生效,不会生成符号,外部不可调用
- inline是类似宏的形式,建议编译器将其在调用处展开,但是仅有建议,是否真正当作inline还是看编译器分析结果
- 如果函数过长或者存在递归等,就不会展开,仅会当作正常函数编译,还会生成符号,除非添加static
7. volatile
- 编译器会对某些变量优化,可能存在将变量读入寄存器后,之后取值直接从寄存器取
- 使用volatile要求编译器对变量的取值每次都从内存中取,不要使用cpu缓存
1 | val1 = x; |
- 如上面的实例,如果x没有被设置volatile,val2的赋值会被编译器优化成从寄存器取值,如果其他地方在val1赋值之后修改了值,val2拿不到修改后的值
8. register
- 函数内使用,将变量存放到cpu寄存器中,不需要存放到内存,用于加快速度,和上面的volatile对应
- 对register变量无法取地址,因为此变量在cpu寄存器中,会随时被更改
9. 数组
9.1. 数组的地址
- 数组是一种类型,对数组取地址,仅仅是1级指针,指向数组这个类型,实际值和数组首地址一样,但是类型不一样
1 | int arr[5] = {1, 2, 3, 4, 5}; |
10. float
10.1. 组成和计算
- float一共占用4个字节32位,1位符号位,8位二进制指数位,23位尾数
- 转换过程是将数字转成二进制的带小数点的科学记数法,如
$$ 8.25 = 33 \times 2^{-2} = 100001 \times 2^{-2} = 1.00001 \times 2^3 $$
$$ 8.25 = 8 + 0.25 = 1000 + 0.01 = 1000.01 \times 2^3 $$
- 小数点前面永远是1,所以直接省略掉,只取00001放到后面23位尾数内
- 指数为5,由于8位可以表示
-127 ~ 128
,为了统一就直接取0为127,那么5就是 $127 + 3 = 130 = 0b10000010$ - 前面符号位为0,正数,所以8.25的float为
0 10000010 000010000000000000000000
11. double
11.1. 组成
- 计算参考float
- double占用8个字节64位,1位符号位,11位指数位,52位尾数
三、系统相关
1. 大小端
1.1. 大小端出现的原因
- 对于16位、32位、64位等寄存器,由于人们的读取习惯为从左向右,但是数字又是右边是低位,左边是高位,所以出现了不同硬件厂商定义了不同的结构模式
1.2. 大小端定义详解
- 比如
0x1234
,0x12
是高8位,0x34
是低8位 - 计算机需要从低地址数字低位开始读取,数字从小到大,
0x34
、0x12
,x86和大部分arm架构都是小端模式 - 人们却习惯从高位开始读,所以读成
0x12
、0x34
,网络字节序定义为大端模式
0x000000 | 0x000001 | |
---|---|---|
大端 | 0x12 | 0x34 |
小端 | 0x34 | 0x12 |
四、其他
1. exit和return
- exit用于在程序运行的过程中随时结束程序,exit的参数是返回给OS的。main函数结束时也会隐式地调用exit函数。exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit是结束一个进程,它将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值并退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。
- return是语言级别的,它表示了调用堆栈的返回;return( )是当前函数返回,当然如果是在主函数main, 自然也就结束当前了,如果不是,那就是退回上一层调用。在多个进程时。如果有时要检测上个进程是否正常退出。就要用到上个进程的返回值,依次类推。而exit是系统调用级别的,它表示了一个进程的结束。
- exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。
- 和机器和操作系统有关的一般是: 0为正常退出,非0为非正常退出;
- exit()头文件为
#include <stdlib.h>
2. 二进制符号
2.1. 查看符号
1 | 导出二进制符号表 |
- 符号表中LOCAL代表本地符号,外部不可用。调用也会调用自己的函数,不可被普通hook
- GLOBAL代表导出的符号,可以给外部调用。内部调用也是按照so库加载顺序调用,即可以被hook
2.2. extern “C” 作用
- 一般这个标记是用来标识C++中导出的C函数,但是在符号表中的效果却不一样
Logc_init
函数没有加extern "C"
1 | ... |
Logc_init
函数加extern "C"
1 | ... |
- 可以看到
extern "C"
使函数导出的符号按照C函数的方式,不加前缀和后缀
2.3. 隐藏符号
- 二进制的符号可以通过
strip
命令进行隐藏 - 默认so库的函数如果没加static都会被编译成外部可调用的形式,readelf可以看到是GLOBAL的形式
- 但是如果不strip,动态库的符号可能会影响到其他动态库的引入。比如两个动态库内部都定义了一个
log_init()
函数,并且它们一个是内部使用的函数,不对外暴露,一个是给二进制用的函数。但是因为二进制运行加载顺序导致这个符号冲突了,导致和期望结果不一致。 - 不对外暴露的接口最好隐藏掉符号。
- 隐藏符号使用gcc编译选项
-fvisibility=hidden
,编译出来,所有函数默认是LOCAL形式,strip会隐藏掉 - 但是需要暴露的符号需要在函数前面加上
__attribute__((visibility("default")))
- 加在头文件就可以了
1 |
|
编译
1 | g++ -c libtest.cpp -o libtest.o -fvisibility=hidden -fPIC -Wall -Werror -std=c++11 |
2.4. 符号加载顺序
(1) 二进制正常加载
- 调用的每个so库的符号,都会从前向后查找库中的符号,谁在前调用谁
(2) so库里面调用函数
- 如果此函数是内部未导出函数,会调用自己
- 如果此函数是导出函数,会重新从前向后查找,不会直接用自己
示例
1 | // A.so |
- 如果,
main.cpp
链接A.so
1 | => LD_PRELOAD=./B.so ./main |
五、glibc接口
1. popen 执行命令(跨平台)
1.1. 示例
1 | /** |
六、编译原理
1. 链接过程的重定位
踩坑记
1. redefine错误
- 全局变量在定义必须在c和cpp中,可以在公用头文件以extern声明,不然会被两个同时包含头文件的源文件编译时报重复定义的错误
- 宏定义在头文件中定义被两个源文件编译不会报重复定义的错误
- 宏定义同一个名字在两个头文件定义,内容定义相同不报错,不同则报warning,最新的会覆盖掉老的定义
2. confiture模式的第三方库修改编译生成的库的名字
如libxml2、libcurl等第三方库,如果编译想要换个名字需要将
- 当前目录及子目录的所有
Makefile.am
、Makefile.in
里面的libxxx.la
修改成新的名字libdiy.la
- 重新执行
./configure && make
即可
3. int赋值给unsigned long long的问题
- 当int为负数时,赋值给
unsigned long long
时会将64位前32位变成1,由于补码特性 - 正常取32位,需要使用
unsigned int
赋值
4. 头文件定义static的问题
- 如果头文件定义了static变量(包括函数),效果是每个include的源文件都会存在static变量
- 如果此变量作为进程唯一就会出问题,除非作为源文件唯一,否则需要定义在c文件中