一、C标准库 1. stdio.h 1.1. 字符串格式化输出函数 snprintf() 函数原型
1 2 3 #include <stdio.h> extern int snprintf (char *__restrict __s, size_t __maxlen,const char *__restrict __format, ...)
snprintf是指定长度的格式化输出 返回值为format拼接的长度,不是被写入后字符串的长度 其中的__maxlen可以指定为sizeof(__s),snprintf只会写入__maxlen - 1的字符,并在最后拼接\0 1.2. 文件操作 1) 函数原型 1 2 3 4 5 6 7 8 9 10 11 12 13 FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) ;size_t fread (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream) ;int fclose (FILE *__stream) ;
2. string.h 2.1. 输出家族函数 1 2 3 4 5 6 #include <stdio.h> int printf (const char *format, ...) ; int fprintf (FILE *stream, const char *format, ...) ; int sprintf (char *str, const char *format, ...) ; int snprintf (char *str, size_t size, const char *format, ...) ;
以下函数功能与上面的一一对应相同,只是在函数调用时,把上面的…对应的一个个变量用va_list调用所替代。在函数调用前ap要通过va_start()宏来动态获取。
1 2 3 4 5 6 #include <stdarg.h> int vprintf (const char *format, va_list ap) ;int vfprintf (FILE *stream, const char *format, va_list ap) ;int vsprintf (char *str, const char *format, va_list ap) ;int vsnprintf (char *str, size_t size, const char *format, va_list ap) ;
format格式说明
1 2 3 4 5 6 7 8 9 const char *const str = "abcdefghijklmnopqrstuvwxyz" ;const char *const str5 = "abcde" ;printf ("%.5s\r\n" , str); printf ("%.10s\r\n" , str5); printf ("%5.3s\r\n" , str); printf ("%-5.3s\r\n" , str); printf ("%3.5s\r\n" , str); printf ("%10s\r\n" , str);
2.2. 字符串拷贝 strncpy() 函数原型
1 2 3 #include <string.h> extern char *strncpy (char *__restrict __dest, const char *__restrict __src, size_t __n)
strncpy拷贝字符串,会检测__src里面的结束符,只拷贝到结束符 如果__src长度大于__n,将拷贝__n个字符到__dest,不会赋值结尾\0,需要手动赋值 如果__src长度小于__n,将拷贝strlen(__src)个字符到__dest,剩余空间赋值\0 2.3. 初始化函数 memset() 1 2 3 4 5 6 7 8 9 10 #include <string.h> void *memset (void *__s, int __c, size_t __n) ;
函数是按byte(8 bits)进行初始化的,每个字节均会被初始化为__c取前八位的值。
2.4. 字符串拷贝 strdup() 1 2 3 4 5 6 7 8 #include <string.h> char *__cdecl strdup (const char *_Src) ;
2.5. 字符串比较函数 strcmp和strncpm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <string.h> int __cdecl strcmp (const char *_Str1,const char *_Str2) ;int __cdecl strncmp (const char *_Str1,const char *_Str2, size_t _MaxCount) ;
3. setjmp 3.1. 非局部跳转函数 setjmp()/longjmp() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <setjmp.h> int setjmp (jmp_buf __env) ;void longjmp (jmp_buf __env, int __val) ;
实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <csetjmp> using namespace std;class Rainbow {public : Rainbow () { cout << "Rainbow()" << endl; } ~Rainbow () { cout << "~Rainbow()" << endl; } }; jmp_buf kansas; void oz () { Rainbow rb; longjmp (kansas, 47 ); } int main () { int code = setjmp (kansas); if ((code = setjmp (kansas)) == 0 ) { cout << "Setjmp code " << code << endl; oz (); } else { cout << "Setjmp code " << code << endl; } }
输出
1 2 3 Setjmp code 0 Rainbow() Setjmp code 47
注意事项
在上述实例中发现类没有被析构,这两个函数可以实现跳转但是不会检测类相关,所以类不会被析构 4. dlfcn.h 动态链接库 4.1. dlopen()/dlsym()/dlclose() 打开/加载符号/关闭 动态链接库 示例用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int main () { auto handler = dlopen ("./libtest.so" , RTLD_NOW); if (handler == nullptr ) { LOG_WARN ("Can't open lib {}" , "./libtest.so" ); return -1 ; } auto parser = (ParserFunc)dlsym (handler, "parser" ); const char *dlsym_error = dlerror (); if (dlsym_error) { dlclose (handler); LOG_WARN ("Cannot load symbol 'parser': {}" , dlsym_error); return -1 ; } (void )parser (); dlclose (handler); return 0 ; }
4.2. 加载下一个动态库符号 动态链接库符号查找是按照加载顺序查找的,比如有两个动态链接库有同一个init()函数,哪个库先加载,调用就会使用哪个库中的函数。 如果想要使用下一个动态库中的符号,可以使用RTLD_NEXT作为handle传入 使用RTLD_NEXT需要加编译选项-D_GNU_SOURCE 示例
使用LD_PRELOAD=libtest.so xxx将二进制中调用动态库的函数log_init()hook掉,做一些自己的操作 完成自己逻辑后,调回原来的函数 1 2 3 4 5 6 7 8 9 10 11 extern "C" MYLIB_API int log_init (int pid, const char *szFileName) { LOG_DEBUG ("Hook log_init, pid %d, szFile %s" , pid, szFileName); ... LOG_DEBUG ("call origin init" ); auto origin_func = (func_logcInit)dlsym (RTLD_NEXT, "log_init" ); return origin_func (pid, szFileName); }
5. time.h 时间处理函数 5.1. tm指针释放的问题 tm指针在内部是一个固定的内存,每次调用都会修改此内存的值,外部不用释放 但是需要考虑多线程和多次调用的问题,调用之后前一次结果就无效了 二、语法和类型 1. 32位和64位大小区别 short相当于short intlong相当于long intsize_t相当于unsigned longlong和指针大小都等于编译环境的位数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main (int argC, char * arg[]) { LOG_DEBUG ("Size of void * %d" , sizeof (void *)); LOG_DEBUG ("Size of float %d" , sizeof (float )); LOG_DEBUG ("Size of double %d" , sizeof (double )); LOG_DEBUG ("Size of char %d" , sizeof (char )); LOG_DEBUG ("Size of short %d" , sizeof (short )); LOG_DEBUG ("Size of int %d" , sizeof (int )); LOG_DEBUG ("Size of long %d" , sizeof (long )); LOG_DEBUG ("Size of long long %d" , sizeof (long long )); LOG_DEBUG ("Size of void * %d" , sizeof (void *)); LOG_DEBUG ("Size of float %d" , sizeof (float )); LOG_DEBUG ("Size of double %d" , sizeof (double )); LOG_DEBUG ("Size of char %d" , sizeof (char )); LOG_DEBUG ("Size of short %d" , sizeof (short )); LOG_DEBUG ("Size of int %d" , sizeof (int )); LOG_DEBUG ("Size of long %d" , sizeof (long )); LOG_DEBUG ("Size of long long %d" , sizeof (long long )); return 0 ; }
2. char *a和char a[]的区别 1 2 char *a = "Hello" ;char a[] = "Hello" ;
char *a = "Hello";char a[] = "Hello";字符串所在区域 常量区 栈 可读可写 常量区不可写 可读可写 赋值时刻 编译时确定 运行时确定 存取效率 属于静态存储区,较慢 存于栈上,较快 sizeof(a) 指针的大小,取决于编译环境 5 + 1,字符串长度加'\0'
3. const的使用 1 2 3 4 5 6 7 const char *p; const (char *) p; char * const p; const char *const p; char const *p; (char *) const p; char const *const p;
4. struct声明 4.1. 定义位变量 1 2 3 4 5 6 7 8 9 10 11 12 struct test_t { uint16_t a : 1 ; uint16_t b : 1 ; uint16_t c : 1 ; uint16_t d : 1 ; uint16_t e : 2 ; uint16_t f : 2 ; uint16_t g : 2 ; uint16_t h : 2 ; uint16_t i : 2 ; uint16_t j : 2 ; };
针对小端模式 定义在前面的是低位,即a是16位中的0位 但是小端模式下,低位在低地址,所以修改了a为1,则得到的是内存分布为0x01、0x00,但uint16_t打印还是0x0001 5. 宏定义 5.1. #if宏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifdef WIN32 #else #endif #ifndef WIN32 #else #endif #if defined(WIN32) || defined(__APPLE__) #elif !defined(__linux__) || !__linux__ #else #endif
5.2. 预定义宏 1) 平台相关 (1) windows WIN32: 由头文件minwindef.h定义,一般是判断是否有调用windowsapi_WIN32: 32位和64位程序都有,由编译器指定_WIN64: 只有64位程序才有,由编译器指定(2) linux 1 2 3 4 5 6 7 8 9 => gcc -dM -E - < /dev/null | grep -i linux # define __linux 1 # define __gnu_linux__ 1 # define linux 1 # define __linux__ 1 => gcc -dM -E - < /dev/null | grep -i unix # define __unix 1 # define __unix__ 1 # define unix 1
1 2 3 4 5 6 7 8 9 => clang -dM -E -x c /dev/null | grep -i linux # define __gnu_linux__ 1 # define __linux 1 # define __linux__ 1 # define linux 1 => clang -dM -E -x c /dev/null | grep -i unix # define __unix 1 # define __unix__ 1 # define unix 1
通用写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #if defined(_WIN32) || defined(WIN32) #ifdef _WIN64 #else #endif #elif __APPLE__ #if TARGET_IPHONE_SIMULATOR #elif TARGET_OS_IPHONE #elif TARGET_OS_MAC #else #endif #elif __linux__ #elif __unix #elif __posix #endif
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缓存 如上面的实例,如果x没有被设置volatile,val2的赋值会被编译器优化成从寄存器取值,如果其他地方在val1赋值之后修改了值,val2拿不到修改后的值 8. register 函数内使用,将变量存放到cpu寄存器中,不需要存放到内存,用于加快速度,和上面的volatile对应 对register变量无法取地址,因为此变量在cpu寄存器中,会随时被更改 9. 数组 9.1. 数组的地址 数组是一种类型,对数组取地址,仅仅是1级指针,指向数组这个类型,实际值和数组首地址一样,但是类型不一样 1 2 3 4 5 6 int arr[5 ] = {1 , 2 , 3 , 4 , 5 };auto a = arr; auto b = &arr; auto c = &a; std::cout << ((void *)a == (void *)b) << std::endl; std::cout << ((void *)a == (void *)c) << std::endl;
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 2 3 4 # 导出二进制符号表 readelf -Ws xxx > xxx.elf # 导出汇编指令表 objdump -DS xxx > xxx.obj
符号表中LOCAL代表本地符号,外部不可用。调用也会调用自己的函数,不可被普通hook GLOBAL代表导出的符号,可以给外部调用。内部调用也是按照so库加载顺序调用,即可以被hook 2.2. extern “C” 作用 一般这个标记是用来标识C++中导出的C函数,但是在符号表中的效果却不一样 Logc_init函数没有加extern "C"
1 2 3 4 5 ... 70: 00000000000037c1 78 FUNC GLOBAL DEFAULT 11 subhook_free 71: 0000000000001c90 151 FUNC GLOBAL DEFAULT 11 _Z9Logc_initiPKc 72: 0000000000002ed1 75 FUNC GLOBAL DEFAULT 11 subhook_alloc_code ...
Logc_init函数加extern "C"
1 2 3 4 5 ... 75: 000000000000386b 95 FUNC GLOBAL DEFAULT 11 subhook_remove 76: 0000000000001c80 151 FUNC GLOBAL DEFAULT 11 Logc_init 77: 0000000000002e74 77 FUNC GLOBAL DEFAULT 11 subhook_unprotect ...
可以看到extern "C"使函数导出的符号按照C函数的方式,不加前缀和后缀 2.3. 隐藏符号 二进制的符号可以通过strip命令进行隐藏 默认so库的函数如果没加static都会被编译成外部可调用的形式,readelf可以看到是GLOBAL的形式 但是如果不strip,动态库的符号可能会影响到其他动态库的引入。比如两个动态库内部都定义了一个log_init()函数,并且它们一个是内部使用的函数,不对外暴露,一个是给二进制用的函数。但是因为二进制运行加载顺序导致这个符号冲突了,导致和期望结果不一致。 不对外暴露的接口最好隐藏掉符号。 隐藏符号使用gcc编译选项-fvisibility=hidden,编译出来,所有函数默认是LOCAL形式,strip会隐藏掉 但是需要暴露的符号需要在函数前面加上__attribute__((visibility("default"))) 1 2 3 4 5 6 7 #if WIN32 #define EXPORT_API __declspec(dllexport) #else #define EXPORT_API __attribute__((visibility("default" ))) #endif EXPORT_API int test () ;
编译
1 2 g++ -c libtest.cpp -o libtest.o -fvisibility=hidden -fPIC -Wall -Werror -std=c++11 ...
2.4. 符号加载顺序 (1) 二进制正常加载 调用的每个so库的符号,都会从前向后查找库中的符号,谁在前调用谁 (2) so库里面调用函数 如果此函数是内部未导出函数,会调用自己 如果此函数是导出函数,会重新从前向后查找,不会直接用自己 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 EXPORT_API void func1 () { printf ("A.so func1\r\n" ); func2 (); } EXPORT_API void func2 () { printf ("A.so func2\r\n" ); } EXPORT_API void func2 () { printf ("B.so func2\r\n" ); } int main () { func1 (); }
1 2 3 4 5 6 => LD_PRELOAD=./B.so ./main A.so func1 B.so func2 => ./main A.so func1 A.so func2
五、glibc接口 1. popen 执行命令(跨平台) 1.1. 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static bool cmdPopen (const std::string &cmd, std::string &outStr, std::error_code &ec, const std::vector<int > &exceptCodes = {0 }) { std::string tag = "popen exec '" + cmd + "'" ; LOGI (WHAT ("{}" , tag)); static const int RESULT_BUF_SIZE = 4096 ; auto fp = popen (cmd.c_str (), "r" ); if (fp == nullptr ) { ec.assign (errno, std::system_category ()); return false ; } do { char resultBuf[RESULT_BUF_SIZE] = {0 }; while (fgets (resultBuf, RESULT_BUF_SIZE, fp) != nullptr ) { outStr += resultBuf; } auto ret = ferror (fp); if (ret != 0 ) { LOGI (WHAT ("{}, fgets error, ec {}, ignore it" , tag, std::to_string (std::error_code (ret, std::system_category ())))); clearerr (fp); } } while (false ); auto ret = pclose (fp); fp = nullptr ; auto exitStatus = WIFEXITED (ret); auto err = WEXITSTATUS (ret); if (!exitStatus) { LOGW (WHAT ("{}, pclose failed" ), REASON ("process is not exit" ), WILL ("ignore it" )); } if (std::find (exceptCodes.begin (), exceptCodes.end (), err) == exceptCodes.end ()) { ec.assign (err, std::system_category ()); return false ; } outStr = "ignore errno:" + std::to_string (std::error_code (err, std::system_category ())); return true ; }
六、gcc编译详解 1. 选项解释 编译选项
-Os: 类似于-O2.5但是不缩减代码尺寸-fpie: pie功能看后面解释,给可执行文件使用,e代表executable-fno-pie: 关闭pie功能-fpic: 和pie类似,给链接库使用,c代表code-s: strip掉所有符号-fsanitize=address: 监听内存泄漏,需要同时加上-lasan并且保证已经安装libasan-fno-stack-protector: gcc默认会添加栈安全检查,但是这个会依赖glibc的库,导致undefined reference to `__stack_chk_fail’的问题-MD: 编译时输出依赖关系到x.d,一般是头文件包含关系-Werror: 所有warning当作error处理-nostdinc: 不搜索默认路径头文件,一般是嵌入式或内核开发使用-m32: 32位编译链接选项,针对ld
-pie: 链接选项,需要和-fpie一起使用,不过不加好像是默认看编译选项-no-pie: 链接选项,关闭pie,同样需要和-fno-pie一起使用,不过不加好像是默认看编译选项-m elf_i386: 生成32位的可执行文件--exclude-libs=x1.a,x2.a: 排除静态库的符号,主要在编译动态库时防止符号冲突,并保证静态库调用的符号是正常的1.1. PIE技术 PIE(position-independent executable)是一种生成地址无关可执行程序的技术。编译器在生成代码时,会添加特殊指令,让内部跳转时不关心内存在哪里加载,也就可以将二进制在内存中随即地址加载
一般技术为添加__x86.get_pc_thunk.ax函数,使用当前的地址加上偏移计算出对应调用地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 00280000 <start_kernel>: ... 280004: e8 eb 02 00 00 call 2802f4 <__x86.get_pc_thunk.bx> 280009: 81 c3 7b 06 00 00 add $0x67b,%ebx ... Disassembly of section .text.__x86.get_pc_thunk.bx: 002802f4 <__x86.get_pc_thunk.bx>: 2802f4: 8b 1c 24 mov (%esp),%ebx 2802f7: c3 ret 00280684 <_GLOBAL_OFFSET_TABLE_>: ...
__x86.get_pc_thunk.bx实现是将esp内存的地址赋值给ebx,esp存的地址是下一条指令所在的地址,也就是上面的280009: 81 c3 7b 06 00 00 add $0x67b,%ebx的地址0x280009。对这个地址添加0x67b得到0x280684也就是_GLOBAL_OFFSET_TABLE_的地址。这段汇编就是将全局偏移表的地址存到ebx中,后面进行使用。
使用当前地址加偏移的方式就可以让二进制在加载内存时不用在特定地址加载。
此方式只需要给全局变量和静态变量使用,因为函数地址本身就是偏移得来的,
2. 查看预定义宏 1 2 3 4 5 => gcc -dM -E - < /dev/null # define __SSP_STRONG__ 3 # define __DBL_MIN_EXP__ (-1021) # define __UINT_LEAST16_MAX__ 0xffff ...
3. 单函数优化级别设定 全局优化级别设定后,可以在编译时指定单个函数的优化级别 1 2 3 4 static int ip_rcv_finish (struct net *net, struct sock *sk, struct sk_buff *skb) __attribute__ ((optimize("O0" ))) ;int ip_rcv_finish (struct net *net, struct sock *sk, struct sk_buff *skb) { ... }
或者指定一批,pragma后面的函数都会应用此优化 1 2 3 4 #pragma GCC optimize("O0" ) static int ip_rcv_finish (struct net *net, struct sock *sk, struct sk_buff *skb) { ... }
4. 编译过程的报错解释 unused-result: 返回值未使用pointer-sign: 指针符号错误int-conversion: 整形类型转换unused: 变量未使用declaration-after-statement: 语句在定义前面,这个一般在C90标准中有问题5. 实例 1) 编译静态库 1 2 3 4 5 6 gcc -c c-v1-static/c.c -o c-v1-static/c.o # 创建静态库 # r replace, 存在同名文件则替换 # s symbol, 更新symbol talbe,更新符号表 # c create, 归档的文件不存在不告警,直接创建进去 ar rsc c-v1-static/c.a c-v1-static/c.o
2) 编译动态库 1 gcc -shared -fPIC main.cpp -o main.so
踩坑记 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文件中