Skip to content

Latest commit

 

History

History
2057 lines (1402 loc) · 48.6 KB

C99.md

File metadata and controls

2057 lines (1402 loc) · 48.6 KB

...menustart

...menuend

C 99

0 GCC 编译流程

GCC编译C源码有四个步骤:

预处理-----> 编译 ----> 汇编 ----> 链接

  1. Pre-processing
    • 编译器将C源代码中的包含的头文件如stdio.h编译进来
    • gcc -E test.c -o test.i
  2. Compiling
    • 编译而不进行汇编,生成汇编代码
    • gcc -S test.i -o test.s
  3. Assembling
    • 把编译阶段生成的”.s”文件转成二进制目标代码
    • gcc -c test.s -o test.o
  4. Link
    • gcc test.o -o test
    • 如不指定 -o, 生成默认的可执行文件 a.out

gcc test.c 会执行完整的4步,最后生成 可执行文件 a.out

在没有特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找库函数 ,去获取函数”printf” 的实现 。

可以用ldd命令查看动态库加载情况, Mac上可以使用 otool:

otool -L test
test:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)

0.1 C语言各种版本

主版本 C89 C99 C11
gcc 参数 -ansi 或 -std=c90 -std=c99 -std=c11
gcc带c扩展参数 -std=gnu90 -std=gnu99 -std=gnu11

0.2 gcc 部分选项

选项 说明
-std=<value> Language standard to compile for
-S Only run preprocess and compilation steps
-c Only run preprocess, compile, and assemble steps
-ansi 支持符合ANSI标准的C程序
-pedantic 允许发出ANSI C标准所列的全部警告信息
-pedantic-error 允许发出ANSI C标准所列的全部错误信息
-w 关闭所有告警
-Wall 允许发出Gcc提供的所有有用的报警信息
-werror 把所有的告警信息转化为错误信息,并在告警发生时终止编译过程
  • example:
    • gcc -std=c89 -pedantic-errors hello.c

GCC 生成动态链接库

$ gcc -shared -fpic test.c -o hello.so
$ file hello.so
hello.so: Mach-O 64-bit dynamically linked shared library x86_64

1 数据类型

1.1 整数

  • char: 有符号 8 位整数。
  • short: 有符号 16 位整数。
  • int: 有符号 32 位整数。
  • long: 在 32 位系统是 32 整数 (long int),在 64 位系统则是 64 位整数。
  • long long: 有符号 64 位整数 (long long int)。
    • long longs are guaranteed to be at least 64 bits
  • bool: _Bool 类型,8 位整数,在 stdbool.h 中定义了 bool / true / false 宏便于使
在不同系统上 char 可能代表有符号或 符号 8 位整数,因此建议使  
unsigned char / signed char 来表 具体的类型。
字符常量默认是 个 int 整数
char c = 'a';  // size(char)=1, size('a')=4
获取指针大小, 应用 sizeof(char*) 这样的方法,
省去处理 32 位和 64 位的区别。
"##" 运算符表 把左和右结合在 起,作为 个符号。

1.2 浮点数

  • float: 32 位 4 字节浮点数,精确度 6。
  • double: 64 位 8 字节浮点数,精确度 15。
  • long double: 80 位 10 字节浮点数,精确度 19 位。
浮点数默认类型是 double,可以添加后缀 F 来表  float,
L 表  long double,可以局部省略
C99 提供了复数支持
直接在 float、double、long double 后添加 _Complex 即可表示复数

1.3 枚举

enum color { black=1, red = 5, green, yellow=1 };
enum color b = black;
printf("black = %d\n", b);
enum color r = red;
printf("red = %d\n", r);
enum color g = green;
printf("green = %d\n", g);

black = 1
red = 5
green = 6
  • 枚举成员的值可以相同
  • 通常省略枚举名 来代替宏定义常量。
enum { BLACK = 1, RED, GREEN = 1, YELLOW };
printf("black = %d\n", BLACK);

2 literal 字面值

2.3 字符串常量

字符常量默认是 int 类型,除非前置 L 表示wchar_t 宽字符类型。

wchar_t wc = L'中';

2.4 字符串常量

C 中的字符串是以 NULL (也就是 \0) 结尾的 char 数组。

strlen 和 sizeof 表 的含义不同

同样可以使用 L 前缀声明一个宽字符串, wchar_t 字符串以一个 4 字节的 NULL结束 00 00 00 00

wchar_t* ws = L"中国 ";

4. 运算符

4.2 sizeof

不要 int 代替 size_t,因为在 32 位和 64 位平台 size_t 长度不同。

4.3 逗号运算符

2元运算符, 确保操作数从左到右被顺序处理,并返回右操作数的值和类型。

4.4 优先级

  1. "." 优先级高于 "*"。
    • *p.f <=> *(p.f)
  2. "[]" 高于 "*"
    • int *ap[] <=> int *(ap[])
  3. "==" 和 "!=" 高于赋值符
    • c = getchar() != EOF <=> c = (getchar() != EOF)
  4. 逗号运算符在所有运算符中优先级最低
    • i = 1, 2 <=> (i = 1), 2

5. 语句

int i = ({ char a = 'a'; a++; a; });
printf("%d\n", i);  // 98

最后 个表达式被当做语句块的返回值。

6. 函数

函数只能被定义一次,但可以被多次 "声明" 和 "调用"。

6.1 嵌套

gcc 支持持嵌套函数扩展。

typedef void(*func_t)();
func_t test()
{
    void func1()
    {
        printf("%s\n", __func__);
    };
    return func1;
}
int main(int argc, char* argv[])
{
    test()();
    return EXIT_SUCCESS;
}

6.2 类型

typedef void(func_t)();   // 函数类型
typedef void(*func_ptr_t)();   // 函数指针类型

void test() {
    printf("%s\n", __func__);
}
int main(int argc, char* argv[])
{
    func_t* func = test;        // 声明一个函数指针
    func_ptr_t func2 = test;    // 函数指针
    void (*func3)();            // 声明 包含函数原型的函数指针变量
    func3 = test;
    func();
    func2();
    func3();
    return EXIT_SUCCESS;
}

6.3 调用

C 函数默认采用 cdecl 调用约定,参数从右往左入栈,且由调用者负责参数出栈和清理。

printf("call: %d, %s\n", a(), s()); //会先执行 s(),再执行 a()

可以通过传递 "指针的指针" 来实现传出参数.

6.4 修饰符

C99 修饰符:

  • extern: 默认修饰符
    • Applied to a function definition, has global scope (and is redundant)
      • 用于函数表示 "具有外部链接的标识符",这类函数可 于任何程序文件
    • Applied to a variable, defined elsewhere
      • 用于变量表示该变量在其他单元中定义
  • static:
    • At the function level, visible only in this file
      • 修饰函数, 函数仅在其所在编译单元 (源码文件) 中可见
      • 修饰变量,变量仅在 该文件可见
        • 文件作用域变量,它本身必然是一个静态变量
        • 所以对于文件作用域的变量,关键字static的作用不是表明存储时期,而是链接类型
    • Inside a function, retains its value between calls
      • 函数内部,局部静态变量
    • PS. C static -- 最名不符实的关键字
      • c++ 中赋予了 static 第三个作用
  • inline:
    • 建议编译器将函数代码内联到调用处,但编译器可自主决定是否完成。
    • 通常包含循环或递归函数不能被定义为 inline 函数。

GNU inline 相关说明:

  • static inline: 内链接函数,在当前编译单元内内联。不过 -O0 时依然是 call
  • inline: 外连接函数,当前单元内联,外部单元为普通外连接函数 (头文件中不能添加 inline 关键字)。

6.5 可选性自变量

使用可选性自变量实现变参。

  • va_start: 通过可选性自变量前一个参数位置来初始化 va_list 变量类型指针。
  • va_arg: 获取当前可选自变量值,并将指针移到下一个可选自变量。
  • va_end: 当不再需要自变量指针时调 。
  • va_copy: 现存的自变量指针 (va_list) 来初始化另一指针。

7. 数组

7.1 可变长度数组

如果数组具有 动 存周期,且没有 static 修饰符,那么可以 常量表达式来定义数组

void test(int n) {
    int x[n];
    for (int i = 0; i < n; i++) {
        x[i] = i; 
    }
}

7.3 初始化

初始化规则:

  • 如果数组为静态生存存周期,那么初始化器必须是常量表达式。
  • 如果提供初始化器,那么可以不提供数组长度,由初始化器的最后一个元素决定
  • 如果同时提供长度和初始化器,那么没有提供初始值的元素都被初始化为 0 或 NULL
int x[] = { 1, 2, 3 };
int y[5] = { 1, 2 };
int a[3] = {};

int z[][2] = {
    { 1, 1 },
    { 2, 1 },
    { 3, 1 },
};

我们还可以在初始化器中初始化特定的元素。

int x[] = { 1, 2, [6] = 10, 11 };
int len = sizeof(x) / sizeof(int);
for (int i = 0; i < len; i++)
{
    printf("x[%d] = %d\n", i, x[i]);
}

#---

x[0] = 1
x[1] = 2
x[2] = 0
x[3] = 0
x[4] = 0
x[5] = 0
x[6] = 10
x[7] = 11

二维数组通常也被称为 "矩阵 (matrix)",相当于一个 row * column 的表格。

7.6 数组参数

当数组作为函数参数时,总是被隐式转换为指向数组第一个元素的指针,也就是说我们再也无法通过 sizeof 获得数组的实际长度了。

void test(int x[])

8. 指针

void* 称为万能指针.

8.4 限定符

限定符 const 可以声明 "类型为指针的常量" 和 "指向常量的指针" 。

int b=500;
const int *a= &b;  //[1] const 在 *左边, 修饰指针指向的变量
int const* a= &b;  //[2] , *a=3 非法
int* const a= &b;  //[3] , const 在*右边,修饰指针本身,a++错误

具有 restrict 限定符的指针被称为限定指针。告诉编译器在指针生存周期内,只能通过该指针修改对象,但编译器可自主决定是否采纳该建议。

8.5 数组指针

指向数组本身的指针, 指向第一元素的指针。

int x[] = { 1, 2, 3 };
int(*p)[] = &x; // &x 返回数组指针
// x 和 &x 完全不同, x和 &x[0] 近似

9. 结构

9.1 不完整结构

结构类型无法把自己作为成员类型,但可以包含 "指向自己类型" 的指针成员。

struct list_node
{
    struct list_node* prev;
    struct list_node* next;
    void* value;
};

定义不完整结构类型,只能使用小标签,像下面这样的 typedef 类型名称是不行的。

typedef struct
{
    list_node* prev;
    list_node* next;
    void* value;
} list_node;

# 编译出错:

结合起来用吧。

typedef struct node_t
{
    struct node_t* prev;
    struct node_t* next;
    void* value;
} list_node;

# node_t 和 list_node 可以命名可以相同

9.2 匿名结构

typedef struct
{
    struct {
        int length;
        char chars[100];
    } s;
    int x;
} data_t;
struct { int a; char b[100]; } d = { .a = 100, .b = "abcd" };

9.3 成员偏移量

利用 stddef.h 中的 offsetof 宏可以获取结构成员的偏移量。

typedef struct
{
    int x;
    short y[3];
    long long z;
} data_t;
int main(int argc, char* argv[])
{
    printf("x %d\n", offsetof(data_t, x));
    printf("y %d\n", offsetof(data_t, y));
    printf("y[1] %d\n", offsetof(data_t, y[1]));
    printf("z %d\n", offsetof(data_t, z));
    return 0;
}


# x 0
# y 4
# y[1] 6
# z 16

# 注意:输出结果有字节对齐。如果z 是int 类型, offset z 是 12 

9.4 定义

定义结构类型有多种灵活的方式。

int main(int argc, char* argv[])
{
    /* 直接定义结构类型和变量 */
    struct { int x; short y; } a = { 1, 2 }, a2 = {}; 
    printf("a.x = %d, a.y = %d\n", a.x, a.y);
    
    /* 函数内部也可以定义结构类型 */ 
    struct data { int x; short y; };
    struct data b = { .y = 3 };
    printf("b.x = %d, b.y = %d\n", b.x, b.y);
    
    /* 复合字面值 */
    struct data* c = &(struct data){ 1, 2 }; 
    printf("c.x = %d, c.y = %d\n", c->x, c->y);
    
    /* 也可以直接将结构体类型定义放在复合字面值中 */
    void* p = &(struct data2 { int x; short y; }){ 11, 22 };
    
    /* 相同内存布局的结构体可以直接转换 */
    struct data* d = (struct data*)p; 
    printf("d.x = %d, d.y = %d\n", d->x, d->y);
    return 0;
}

9.5 初始化

未被初始化器初始化的 成员将被设置为 0。

typedef struct
{
    int x;
    short y[3];
    long long z;
} data_t;
int main(int argc, char* argv[])
{
    data_t d = {};
    data_t d1 = { 1, { 11, 22, 33 }, 2LL };
    data_t d2 = { .z = 3LL, .y[2] = 2 };
    return 0;
}

9.6 弹性结构成员 C99

通常又称作 “不定长结构”,就是在结构体尾部声明一个未指定长度的数组。 用 sizeof 运算符时,该数组未计入结果。

typedef struct string
{
    int length;
    char chars[];
} string;

int main(int argc, char * argv[])
{
    int len = sizeof(string) + 10;  // 计算所需的长度。
    char buf[len];  // 从栈上分配所需的内存空间。
    
    string *s = (string*)buf;  // 转换成 struct string 指针。
    s->length = 9;
    strcpy(s->chars, "123456789");
    
    printf("%d\n%s\n", s->length, s->chars);
    return 0;
}

# 9
# 123456789

10. 联合

一个常用联合用法:

union  { int x; struct {char a, b, c, d;} bytes; } n = { 0x12345678 };
printf("%#x => %x, %x, %x, %x\n", n.x, n.bytes.a, n.bytes.b, n.bytes.c, n.bytes.d);

# 0x12345678 => 78, 56, 34, 12

11. 位字段

可以把结构 或 联合的多个成员 压缩存储在 一个字段中, 以节约内存

struct {
    unsigned int year : 22;
    unsigned int month : 4;
    unsigned int day : 5;
} d = { 2010, 4, 30 };
printf("size: %d\n", sizeof(d));
printf("year = %u, month = %u, day = %u\n", d.year, d.month, d.day);

# size: 4
# year = 2010, month = 4, day = 30

用来做标志位也挺好,比用 位移运算符更直观,更节省内存

int main(int argc, char * argv[])
{
    struct {
        bool a: 1;
        bool b: 1;
        bool c: 1;
    } flags = { .b = true };
    printf("%s\n", flags.b ? "b.T" : "b.F");
    printf("%s\n", flags.c ? "c.T" : "c.F");
    return 0;
}

12. 声明

12.1 类型修饰符

  • const:
  • volatile: 目标可能被其他线程或事件修改,使用该变量前,都需从主存重新获取
  • restrict: 修饰指针,出了该指针, 不能用其他任何方式修改目标对象.

12.2 链接类型

UDT: 用户自定义类型 , 结构,联合,枚举等

元素 储存类型 作用域 生存周期 链接类型
全局UDT 文件 内链接
嵌套UDT 内链接
局部UDT 程序块 无链接
全局函数,变量 extern 文件 永久 外链接
静态全局函数,变量 static 文件 永久 内链接
局部变量,常量 auto 程序块 临时 无链接
静态局部变量,常量 static 程序块 永久 无链接
全局常量 文件 永久 内链接
静态全局常量 static 文件 永久 内链接
宏定义 文件 内链接

内链接和外链接:

编译的时候,是以源文件为单位,编译成一个个的obj文件, 然后再通过链接器把不同的obj文件链接起来.

如果一些变量或函数的定义是内连接的话,链接器链接的时候就不会拿它们去与obj比较看有重复定义不,一个源文件中的extern声明的变量或函数也不能使用另外一个源文件中的内连接的变量或函数.

而如果是外连接的话则需要在不同的obj中比较是否有重定义的.除了做这样的检查外,链接器还会查看通过extern修饰的变量或函数声明在其他obj中的定义.

extern外部声明

假设有两个源文件:

one.c 中:

int number = 123;  //number的定义 .或者写成extern number =123;
//当有赋值时,实际上extern失去了应有的作用.所以加不加没影响.

void Print() { TODO }

tow.c 中:

extern int number; //这就是所谓的外部声明
// 此处extern不可省.另外此处绝对不能赋值.
// 如果写成extern int number = 88; 链接时会报错: 重复定义.

extern void Print();  //此处extern可以省略, 因为是函数原型 ?.

static 内部连接

上面的例子中我们知道one.c 和two.c 中同时写上int number=xx会出错, 如果都加上static 关键字,则又正常了。

因为静态变量是内部连接,链接器不会去看不同obj文件中是否有重名的静态变量。

12.3 隐式初始化

具有静态生存周期的对象, 会被初始化为 默认值 0.

13. 预处理

13.2 宏函数

利用宏可以定义伪函数, 通常用 ({...}) 来组织多行语句,

最后一个表达式作为返回值(无return, 且有个";"结束)

#include <stdio.h>
#define test(x, y) ({   \
    int _z = x + y;     \
    _z; })
    
int main(int argc, char* argv[])
{
    printf("%d\n", test(1, 2));
    return 0;
}

gcc -E test.c 查看展开结果:

int main(int argc, char* argv[])
{
     printf("%d\n", ({ int _z = 1 + 2; _z; }));
         return 0;
}

13.3 可选性变量

VA_ARGS

13.4 字符串化运算符

单元运算符 # 将一个宏参数 转换为 字符串

#define test(name) ({   \
    printf("%s\n", #name); })
int main(int argc, char* argv[])
{
    test(main);
    return 0;
}

13.5 粘贴记号运算符

二元运算符 ## 将 左右两个操作数 结合成一个 记号

#define test(name, index) ({                          \
    int i, len = sizeof(name ## index) / sizeof(int); \
    for (i = 0; i < len; i++)
    {   \ 
        printf("%d\n", name ## index[i]); \
    }})
    
int main(int argc, char* argv[])
{
    int x1[] = { 1, 2, 3 };
    int x2[] = { 11, 22, 33, 44, 55 };
    test(x, 1);
    test(x, 2);
    return 0;
}

展开:

int main(int argc, char* argv[])
{
    int x1[] = { 1, 2, 3 };
    int x2[] = { 11, 22, 33, 44, 55 };
    ({ int i, len = sizeof(x1) / sizeof(int); for (i = 0; i < len; i++) {
        printf("%d\n", x1[i]); }});
    ({ int i, len = sizeof(x2) / sizeof(int); for (i = 0; i < len; i++) {
        printf("%d\n", x2[i]); }});
    return 0; 
}

13.6 条件编译

#if ... 
#elif ... 
#else ... 
#endif

#define
#undef

#ifdef
#ifndef
#define V1
#if defined(V1) || defined(V2)
    printf("Old\n");
#else
    printf("New\n");
#endif
#undef V1
#define V1
#ifdef V1
    printf("Old\n");
#else
    printf("New\n");
#endif
#undef A

13.7 typeof

使用 GCC 扩展 typeof 可以获知参数的类型

13.8 其他

一些常用的特殊常量

  • #error "message" : 定义一个编译器错误信息
  • DATE : 编译日期字符串
  • TIME : 编译时间字符串
  • FILE : 当前源码文件名
  • LINE : 当前源码行号
  • func : 当前函数名称

14. 调试

assert

不过在编译 release 版本时,记得加上 -DNDEBUG.


第三部分: 系统

3. Core Dump

利用 setrlimit 函数我们可以将 "core file size" 设置成一个非 0 值,这样就可以在崩溃时自动生成 core 文件了。(可参考 bshell ulimit 命令)

#include <sys/resource.h>
void test() {
    char* s = "abc";
    *s = 'x'; 
}

int main(int argc, char** argv)
{
    struct rlimit res = { .rlim_cur = RLIM_INFINITY, 
        .rlim_max = RLIM_INFINITY };
    setrlimit(RLIMIT_CORE, &res);
    test();
    return (EXIT_SUCCESS);
}

On Mac, you can find the core dump info by "Console" app.

另外,参看 GDB 内的 core dump。

4. Thread

线程对于 Linux 内核来说就是一种特殊的 "轻量级进程"。如同 fork 处理子进程一样,当线程结束时,它会维持一个最小现场,其中保存有退出状态等资源,以便主线程或其他线程调用thread_join获取这些信息。如果我们不处理这个现场,那么就会发生内存泄露。

void* test(void* arg)
{
    printf("%s\n", (char*)arg);
    return (void*)0;
}
int main(int argc, char** argv)
{
    pthread_t tid;
    pthread_create(&tid, NULL, test, "a");
    sleep(3);
    return (EXIT_SUCCESS);
}

编译后,我们 Valgrind 检测 下。

valgrind --leak-check=full ./test

==11802== Memcheck, a memory error detector

发现内存泄漏,把 sleep(3) 换成

    void* state;
    pthread_join(tid, &state);
    printf("%d\n", (int)state);

泄漏没有了。可见 pthread_join 和 waitpid 之类的函数作用类似,就是获取状态,并通知内核完全回收相关内存区域。

在实际开发中,我们并不是总要关心线程的退出状态。例如异步调用,主线程只需建立线程,然后继续自己的任务。这种状况下,我们可以用 "分离线程 (detach)"来通知内核无需维持状态线程, 直接回收全部内存。

pthread_detach 函数分离线程:

int main(int argc, char** argv)
{
    pthread_t tid;
    pthread_create(&tid, NULL, test, "a");
    pthread_detach(tid);
    sleep(3);
    return (EXIT_SUCCESS);
}

当然,也可以在 thread function 中调用 。

void* test(void* arg)
{
    printf("%s\n", (char*)arg);
    pthread_detach(pthread_self());
    return NULL;
}

或者使 线程属性。

int main(int argc, char** argv)
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_t tid;
    pthread_create(&tid, &attr, test, "a");
    sleep(3);
    pthread_attr_destroy(&attr);
    return (EXIT_SUCCESS);
}

4.2 Cancel

pthread_cancel 是个危险的东东,天知道会在哪旮旯停掉线程。

4.3 Synchronization

建议开发中总是设置 PTHREAD_MUTEX_RECURSIVE 属性,避免死锁。

5. Signal

5.1 Payload

5.2 Blocking

6. Zombie Process

6.1 fork + fork

6.2 signal

6.3 sigaction

7. Dynamic Linking Loader

在运行时动态载入库 (.so),并调用其中的函数。

7.1 动态库

我们调用的目标函数就是 testfunc。

#mylib.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void testfunc(const char* s, int x)
{
    printf("testfunc call.\n");
    printf("%s, %d\n", s, x);
}

编译成动态库。

$ gcc -fPIC -shared -o libmy.so mylib.c
$ nm libmy.so
... ...
0000000000000f20 T _testfunc

7.2 调用

8. Unit Testing

CUnit

9. libmm: Memory Pool

Memory Pool 的好处是不在堆和栈上分配,可以重复使用,避免多次向内核请求分配和释放内存,一定程度上提 了性能。另外只需释放整个 Pool即可完成所有的内存释放,避免内存泄露的发生。

安装 libmm 库:

$ sudo apt-get libmm14 libmm-dev libmm-dbg

10. libgc: Garbage Collector

C 一样有好用 、成熟,高效的垃圾回收库 —— libgc。

11. libconfig: Configuration File

配置文件很重要,INI太弱,XML 太繁复,Linux *.conf 很酷。

$ sudo apt-get install libconfig8 libconfig8-dev

12. libevent: Event Notification

libevent 貌似是 Linux 下写高性能服务器的首选组件。当然也可以用epoll 写.

第四部分: 工具

1. GCC

1.1 预处理

gcc -E main.c -o main.i
  • 参数 -Dname 定义宏 (源文件中不能定义该宏)
  • -Uname 取消 GCC 设置中定义的宏。
  • -Idirectory 设置头文件(.h)的搜索路径。
    • gcc -g -I./lib -I/usr/local/include/cbase main.c mylib.c
  • 查看依赖文件
    • gcc -M -I./lib main.c
    • gcc -MM -I./lib main.c # 忽略标准库

1.2 汇编

gcc -S main.c

使用 -fverbose-asm 参数可以获取变量注释。如果需要指定汇编格式,可以使用 "-masm=intel" 参数。

1.3 链接

参数 -c 仅生成目标文件 (.o),然后需要调用链接器 (link) 将多个目标文件链接成单个可执行文件.

gcc -g -c main.c mylib.c
  • 参数 -l 链接其他库
    • 比如 -lpthread 链接 libpthread.so
    • 或指定 -static 参数进行静态链接
  • 还可以直接指定链接库 (.so, .a) 完整路径
    • gcc -g -o test main.c ./libmy.so ./libtest.a
  • 或用 -L 指定库搜索路径
    • gcc -g -o test -L/usr/local/lib -lgdsl main.c

1.4 动态库

使用 "-fPIC -shared" 参数生成动态库。

gcc -fPIC -c -O2 mylib.c
gcc -shared -o libmy.so mylib.o

静态库则需要借助 ar 工具将多个目标文件 (.o) 打包。

$ gcc -c mylib.c
$ ar rs libmy.a mylib.o
ar: creating archive libmy.a

1.5 优化

  • 参数 -O0 关闭优化 (默认)
  • -O1 (或 -O) 让可执文件更小 ,速度更快;
  • -O2 采用几乎所有的优化手段
gcc -O2 -o test test.c mylib.c

1.6 调试

参数 -g 在目标文件 (.o) 和执行文件中生成符号表和源代码行号信息,以便使用 gdb 等工具进行调试。

gcc -g -o test test.c mylib.c

参数 -pg 会在程序中添加性能分析 (profiling) 函数,用于统计程序中最耗费时间的函数。

程序执行后,统计信息保存在 gmon.out 文件中,可以用 gprof 命令查看结果.(Mac上 dSYM 文件?)

gcc -g -pg test.c mylib.c

2. GDB

2.0 Codesigning the GDB on Mac

  1. Codesigning requires a certificate
    • Keychain Access -> Certificate Assistant -> Create a Certificate...
    • a Choose a name for the new certificate (eg "gdb-cert" )
    • b Set "Identity Type" to "Self Signed Root"
    • c Set "Certificate Type" to "Code Signing"
    • d Activate the "Let me override defaults" option
    • "Continue" until the "Specify a Location For The Certificate" screen appears,then set "Keychain" to "System"
    • Click on "Continue" until the certificate is created
  2. double-click on the created certificate, and set "Code Signing" to "Always Trust"
  3. Exit the Keychain Access application , very important
  4. codesign -f -s "gdb-cert" /path2/gdb
  5. 让刚刚添加的证书生效需要重启taskgated服务:sudo killall taskgated , also very important

#include <stdio.h>
int test(int a, int b)
{
    int c = a + b;
    return c; 
}
int main(int argc, char* argv[])
{
    int a = 0x1000;
    int b = 0x2000;
    int c = test(a, b);
    printf("%d\n", c);
    printf("Hello, World!\n");
    return 0; 
}

compile:

gcc -g -o hello hello.c

start debug:

gdb ./hello
GNU gdb (GDB) 7.10.1
Copyright (C) 2015 Free Software Foundation, Inc.
...

2.1 源码

在调试过程中查看源代码是必须的。list (缩写 l) 支持多种格式查看源码。

(gdb) l
1    
2    #include <stdio.h>
3    int test(int a, int b)
4    {
5        int c = a + b;
6        return c; 
7    }
8    int main(int argc, char* argv[])
9    {
10        int a = 0x1000;
(gdb) l 3,10  #显示特定范围的源代码
(gdb) l main  # 显示特定函数源码

可以 如下命令修改源代码显行数。

(gdb) set listsize 50

2.2 断点

可以使用函数名或者源代码行号设置断点。

(gdb) b main  #设置函数断点
...
(gdb) b 13      # 设置源代码行断点
...
(gdb) b     # 将下一行设置为断点 (循环、递归等调试很有 )
...
(gdb) tbreak main  #设置临时断点 (中断后失效)
...
(gdb) info breakpoints  #查看所有断点

(gdb) d 3   # delete: 删除断点 (还可用范围 "d 1-3", 无参数时删除全部断点)
(gdb) disable 2  # 禁用断点 (还可以用范围 "disable 1-3")
(gdb) enable 2   # 启用断点 (还可以用范围 "enable 1-3")
(gdb) ignore 2 1  # 忽略 2 号中断 1 次

当然少不了条件式中断

(gdb) b test if a == 10
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000100000f16 in main at hello.c:10
2       breakpoint     keep y   0x0000000100000f39 in main at hello.c:13
4       breakpoint     keep y   0x0000000100000eea in test at hello.c:5
    stop only if a == 10

可以用 condition修改条件

(gdb) condition 4 a == 30
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000100000f16 in main at hello.c:10
2       breakpoint     keep y   0x0000000100000f39 in main at hello.c:13
4       breakpoint     keep y   0x0000000100000eea in test at hello.c:5
    stop only if a == 30

2.3 执行

通常情况下, 我们会先设置main入口断点.

(gdb) b main
Breakpoint 1 at 0x804841b: file hello.c, line 11.
(gdb) r         #开始执行

Starting program: /Users/qibinyi/Documents/hello 
Breakpoint 1, main (argc=1, argv=0x7fff5fbff650) at hello.c:11
11            int a = 0x1000;

(gdb) n         # step over
12                int b = 0x2000;

(gdb) n
13                int c = test(a, b);

(gdb) s         # step in
test (a=4096, b=8192) at hello.c:6
6            int c = a + b;

(gdb) finish    # Step out
Run till exit from #0  test (a=4096, b=8192) at hello.c:6
0x0000000100000f2f in main (argc=1, argv=0x7fff5fbff650) at hello.c:13
13                int c = test(a, b);
Value returned is $1 = 12288

(gdb) c         # Continue: 继续执⾏行,直到下一个断点
Continuing.
12288
Hello, World!
[Inferior 1 (process 2700) exited normally]

2.3 堆栈

查看调⽤用堆栈⽆无疑是调试过程中⾮非常重要的事情。

(gdb) where    # 查看调⽤用堆栈 (相同作⽤用的命令还有 info s 和 bt)
#0  test (a=4096, b=8192) at hello.c:6
#1  0x0000000100000f2f in main (argc=1, argv=0x7fff5fbff650) at hello.c:13

(gdb) frame    # 查看当前堆栈帧,还可显⽰示当前代码
#0  test (a=4096, b=8192) at hello.c:6
6            int c = a + b;

(gdb) info frame    # 获取当前堆栈帧更详细的信息
Stack level 0, frame at 0x7fff5fbff600:
 rip = 0x100000eea in test (hello.c:6); saved rip = 0x100000f2f
 called by frame at 0x7fff5fbff640
 source language c.
 Arglist at 0x7fff5fbff5f0, args: a=4096, b=8192
 Locals at 0x7fff5fbff5f0, Previous frame's sp is 0x7fff5fbff600
 Saved registers:
  rbp at 0x7fff5fbff5f0, rip at 0x7fff5fbff5f8

可以用 frame 修改当前堆栈帧,然后查看其详细信息。

(gdb) frame 1
#1  0x0000000100000f2f in main (argc=1, argv=0x7fff5fbff650) at hello.c:13
13                int c = test(a, b);
(gdb) info frame
...

2.5 变量和参数

(gdb) info locals   #显示局部变量
c = 1

(gdb) info args     #显示函数参数
a = 4096
b = 8192

(gdb) p a       # print (p) 命令可显示局部变量和参数值
$2 = 4096

(gdb) p/x b     # 16 进制输出
$4 = 0x2000

(gdb) p a*b     # 还可计算表达式
$6 = 33554432

x 命令内存输出格式:

  • d: 十进制
  • u: 十进制无符号
  • x: 十六进制
  • o: 八进制
  • t: 二进制
  • c:字符

set variable 可⽤用来修改变量值。

(gdb) set variable a=100
(gdb) info args
a = 100
b = 8192

2.6 内存及寄存器

x 命令可以显⽰示指定地址的内存数据。

格式: x/nfu [address]

  • n: 显⽰内存单位 (组或者行)
  • f:格式(除了print格式外,还有字符串s和汇编i)
  • u: 内存单位 (b: 1字节; h: 2字节; w: 4字节; g: 8字节)。
(gdb) x/8w 0x7fff5fbff5ec       # 按四字节(w)显示 8 组内存数据
0x7fff5fbff5ec:    100    1606415920    32767    3887
0x7fff5fbff5fc:    1    0    0    0

(gdb) x/8i 0x7fff5fbff5ec       # 显示8行汇编指令
   0x7fff5fbff5ec:    add    %al,%fs:(%rax)
   0x7fff5fbff5ef:    add    %dh,(%rax)
   0x7fff5fbff5f1:    idivb  0x7fff5f(%rdi)
   0x7fff5fbff5f7:    add    %ch,(%rdi)
   0x7fff5fbff5f9:    sldt   (%rax)
   0x7fff5fbff5fc:    add    %eax,(%rax)
   0x7fff5fbff5fe:    add    %al,(%rax)
   0x7fff5fbff600:    add    %al,(%rax)

出了 info frame 查看寄存器值之外,还可以用如下指令:

(gdb) info registers        # 查看所有寄存器数据
rax            0x100000f00    4294971136
rbx            0x0    0
rcx            0x7fff5fbff790    140734799804304
rdx            0x7fff5fbff660    140734799804000
...

(gdb) p $eax        # 显示单个寄存器值
$9 = 3840

2.7 反汇编

我对 AT&T 汇编不是很熟悉,还是设置成 intel 格式的好。

set disassembly-flavor intel    # 设置反汇编格式
disass main                     # 反汇编函数

Dump of assembler code for function main:
   0x0000000100000f00 <+0>:    push   rbp
   0x0000000100000f01 <+1>:    mov    rbp,rsp
   0x0000000100000f04 <+4>:    sub    rsp,0x30
   0x0000000100000f08 <+8>:    mov    DWORD PTR [rbp-0x4],0x0
   ...

可以用 "b *address" 设置汇编断点,然后用 si 和 ni 进行汇编级单步执行,这对于分析指针和寻址非常有用。

2.8 进程

查看进程相关信息,尤其是 maps 内存数据是非常有用的。

(gdb) help info proc
Show /proc process information about any running process.
Specify any process id, or use the program being debugged by default.

List of info proc subcommands:

info proc all -- List all available /proc info
info proc cmdline -- List command line arguments of the process
info proc cwd -- List current working directory of the process
info proc exe -- List absolute filename for executable of the process
info proc mappings -- List of mapped memory regions
info proc stat -- List process info from /proc/PID/stat
info proc status -- List process info from /proc/PID/status
...

# not support on Mac OS

2.9 线程

可以在 pthread_create 处设置断点,当线程创建时会生成提示信息。

(gdb) info threads      # 查看所有线程列表
  Id   Target Id         Frame 
* 1    Thread 0x1253 of process 2768 main (argc=1, argv=0x7fff5fbff650) at hello.c:11

(gdb) thread 1      # 切换线程
[Switching to thread 1 (Thread 0x1253 of process 2768)]
#0  main (argc=1, argv=0x7fff5fbff650) at hello.c:11
11            int a = 0x1000;

2.10 其他

调试子进程。

(gdb) set follow-fork-mode child

临时进入 Shell 执行命令,Exit 返回。

(gdb) shell

调试时直接调用函数。

(gdb) call test("abc")

使用 "--tui" 参数,可以在终端窗口上部显示一个源代码查看窗

$ gdb --tui hello

查看命令帮助。

(gdb) help b

最后就是退出命令。

(gdb) q

2.11 Core Dump

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
...

core file size (blocks, -c) 0 意味着在程序崩溃时不会生成 core dump 文件,我们需要修改一下设置,也有不修改配置文件的方法。

#include <stdio.h>
#include <stdlib.h>
void test() {
    char* s = "abc";
    *s = 'x'; 
}
int main(int argc, char** argv)
{
    test();
    return (EXIT_SUCCESS);
}

很显然,我们在 test 里面写了一个不该写的东,这无疑会很严重。生成可执行文件后,执行上面的命令:

$ sudo sh -c "ulimit -c unlimited; ./test"

sh: line 1:  2793 Bus error: 10           (core dumped) ./test

mac 下 core dump 位于 /cores 目录下

$ ls /cores
core.2865

非常不幸,Mac 上 gdb 还不能识别 core 文件.

有用代码

二进制文件读写

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    FILE *pFile;
    long lSize;   // 用于文件长度
    char* buffer; // 文件缓冲区指针
    size_t result;  // 返回值是读取的内容数量


    if((pFile=fopen( "Assembly-CSharp.dll" ,"rb"))==NULL) 
    {
        printf("not open");
        exit(0);
    }
    // obtain file size
    fseek(pFile , 0 , SEEK_END); // 指针移到文件末位
    lSize = ftell(pFile);  // 获得文件长度
    rewind(pFile);  // 函数rewind()把文件指针移到由stream(流)指定的开始处, 同时清除和流相关的错误和EOF标记

    // allocate memory to contain the whole file: 为整个文件分配内存缓冲区
    buffer = (char*) malloc(sizeof(char) * lSize); // 分配缓冲区,按前面的 lSize
    if (buffer == NULL) {fputs("Memory error", stderr); exit(2);}  // 内存分配错误,退出2

    // copy the file into the buffer:  该文件复制到缓冲区
    result = fread(buffer, 1, lSize, pFile); // 返回值是读取的内容数量
    if (result != lSize) {fputs("Reading error", stderr); exit(3);} // 返回值如果不和文件大小,读错误

    // the whole file is now loaded in the memory buffer.
    const char* mask = "LISP" ;

    int off1 = mask[0]*253 +  mask[1] ;
    int off2 = mask[2] ;

    //printf("offset : %d , %d \n" , off1, off2 );
    int i=0;
    for (i= off1 ; i< lSize - off2 ; i++ ) {
        buffer[i] = buffer[i] ^ ( i & 0xFF ) ;
    }

    fclose(pFile);

    // write
    if((pFile=fopen( "Assembly-CSharp_decrypt.dll" ,"wb"))==NULL) 
    {
        printf("not open 2");
        exit(0);
    }

    // write
    fwrite( buffer, lSize ,1,pFile); 
    fclose(pFile);

    free(buffer);
    printf("end\n");
   
    return 0;
}

可变参数宏 C99?

#define DebugLog(  fmt, ... )    \
do { \
printf( fmt, ##__VA_ARGS__ ) ; \
} while (0)

million seconds since 1970, double type, in secs

// double , in secs .  timeIntervalSince1970
// eg. 1480320197.206797
double current_DateNow() {
    struct timeval te;
    gettimeofday(&te, NULL); // get current time
    double milliseconds = (te.tv_sec*1000LL + te.tv_usec/1000.0) / 1000.0; // caculate milliseconds
    //DebugLog("milliseconds: %f , %f \n", milliseconds , [[NSDate date] timeIntervalSince1970] );
    return milliseconds;
}

GUN c Library source code

http://www.gnu.org/software/libc/manual/html_node/

c Programs from Programming Challenges

https://www3.cs.stonybrook.edu/~skiena/392/programs/

错误排除

warning: incompatible integer to pointer conversion assigning to 'char*' from 'int'

char* s = makeS();
...
char* makeS() {
   char* ms ;
   ...
   return ms ;
}

At this point, no declaration of makeS is in scope. So the compiler, using the old C89 rules, assumes an implcit declaration of makeS returning an int.

C陷阱与缺陷 pdf

http://wenjing.ytu.edu.cn/dianzi/jpkc/ziliaoxz/C.pdf