一、Linux 程序编译过程详解
计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如C,C++,Java,另一种是解释型语言,例如Python、Ruby、MATLAB 、JavaScript。
本文将介绍如何将高层的C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤:
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
GCC 工具链介绍
通常所说的GCC是GUN Compiler Collection的简称,是Linux系统上常用的编译工具。GCC工具链软件包括GCC、Binutils、C运行库等。
GCC
GCC(GNU C Compiler)是编译工具。本文所要介绍的将C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。
Binutils
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。这一组工具是开发和调试不可缺少的工具,分别简介如下:
- addr2line:用来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置。
- as:主要用于汇编,有关汇编的详细介绍请参见后文。
- ld:主要用于链接,有关链接的详细介绍请参见后文。
- ar:主要用于创建静态库。为了便于初学者理解,在此介绍动态库与静态库的概念:
- 如果要将多个.o目标文件生成一个库文件,则存在两种类型的库,一种是静态库,另一种是动态库。
- 在windows中静态库是以 .lib 为后缀的文件,共享库是以 .dll 为后缀的文件。在linux中静态库是以.a为后缀的文件,共享库是以.so为后缀的文件。
- 静态库和动态库的不同点在于代码被载入的时刻不同。静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。
- 如果一个系统中存在多个需要同时运行的程序且这些程序之间存在共享库,那么采用动态库的形式将更节省内存。
- ldd:可以用于查看一个可执行程序依赖的共享库。
- objcopy:将一种对象文件翻译成另一种格式,譬如将.bin转换成.elf、或者将.elf转换成.bin等。
- objdump:主要的作用是反汇编。有关反汇编的详细介绍,请参见后文。
- readelf:显示有关ELF文件的信息,请参见后文了解更多信息。
- size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等,请参见后文了解使用size的具体使用实例。
C运行库
C语言标准主要由两部分组成:一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的printf函数便是一个C标准库函数,其原型定义在stdio头文件中。
C语言标准仅仅定义了C标准库函数原型,并没有提供实现。因此,C语言编译器通常需要一个C运行时库(C Run Time Libray,CRT)的支持。C运行时库又常简称为C运行库。与C语言类似,C++也定义了自己的标准,同时提供相关支持库,称为C++运行时库。
准备工作
由于GCC工具链主要是在Linux环境中进行使用,因此本文也将以Linux系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个C语言编写的简单Hello程序作为示例,其源代码如下所示:
编译过程
1. 预处理
预处理的过程主要包括以下过程:
- 将所有的#define删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等。
- 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
- 删除所有注释“//”和“/* */”。
- 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
- 保留所有的#pragma编译器指令,后续编译过程需要使用它们。
使用gcc进行预处理的命令如下:
hello.i文件可以作为普通文本文件打开进行查看,其代码片段如下所示:
2. 编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
使用gcc进行编译的命令如下:
上述命令生成的汇编程序hello.s的代码片段如下所示,其全部为汇编代码。
3. 汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用Binutils中的汇编器as根据汇编指令和处理器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。
使用gcc进行汇编的命令如下:
注意:hello.o目标文件为ELF(Executable and Linkable Format)格式的可重定向文件。
4. 链接
链接也分为静态链接和动态链接,其要点如下:
- 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
- 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
- 在Linux系统中,gcc编译链接时的动态库搜索路径的顺序通常为:首先从gcc命令的参数-L指定的路径寻找;再从环境变量LIBRARY_PATH指定的路径寻址;再从默认路径/lib、/usr/lib、/usr/local/lib寻找。
- 在Linux系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量LD_LIBRARY_PATH指定的路径寻址;再从配置文件/etc/ld.so.conf中指定的动态库搜索路径;再从默认路径/lib、/usr/lib寻找。
- 在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。
由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc链接时默认优先选择动态库,会链接libtest.so,如果要让gcc选择链接libtest.a则可以指定gcc选项-static,该选项会强制使用静态库进行链接。以Hello World为例:
- 如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:
- 如果使用命令“gcc -static hello.c -o hello”则会使用静态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:
链接器链接后生成的最终文件为ELF格式可执行文件,一个ELF可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss等段。
分析ELF文件
1. ELF文件的段
ELF文件格式如下图所示,位于ELF Header和Section Header Table之间的都是段(Section)。一个典型的ELF文件包含下面几个段:
- .text:已编译程序的指令代码段。
- .rodata:ro代表read only,即只读数据(譬如常数const)。
- .data:已初始化的C程序全局变量和静态局部变量。
- .bss:未初始化的C程序全局变量和静态局部变量。
- .debug:调试符号表,调试器用此段的信息帮助调试。
可以使用readelf -S查看其各个section的信息如下:
2. 反汇编ELF
由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。
使用objdump -D对其进行反汇编如下:
使用objdump -S将其反汇编并且将其C语言源代码混合显示出来:
二、串口数据共用体和结构体--转换
嵌入式系统的串口数据传输都是以字节为单位,但有些特殊的数据类型,比如浮点型float a = 231.5,在内存是如何表示的呢?
我们知道,浮点型float数据类型占用4个字节,实际上在内存当中a = 0x43678000,只是嵌入式芯片访问a时,知道a是浮点型数据,所以一次性读取4个字节,而且也按照浮点型的数据表示规定,将a转换为十进制的可读数据231.5。
如果我们从串口接收到4个字节数据{0x43,0x67,0x80,0x00},如何把这4个字节的数据转换为float型呢?
直接令float a = 0x43678000这是不行的(不信的读者可以自行验证),这就是串口通讯当中经常遇到的问题,如果数据传输中包括了浮点型数据,在这里我们可以通过共用体或者结构体来解决。
对于共用体:
typedef union
{
float f;
unsigned char s[4];
}Union_test;
f 的4个字节和s[4]的4个字节是共用一个区域,如果我们令f = 231.5,然后通过监视窗查看s[4]的数值,下面是测试程序:
#include <stdio.h>
//共用体
//float f;//4个字节
//char s[4];//4个字节
typedef union
{
float f;
unsigned char s[4];
}Union_test;
typedef struct st
{
float f1;
}Struct_test;
void main(void)
{
float a = 231.5;
Union_test x;
Struct_test z;
x.f = a;
z = *(Struct_test *)(&(x.s));
printf("z=%.2f\r\n", (double)z.f1);
printf("End of this programme\r\n");
}
监视结果如下所示:
我们同样适用结构体做了相同的实验,将数组s[4]={0x00,0x80,0x67,0x43}的首地址s[0]强制转换赋值给结构体z,最后打印输出的结果也是231.5这里我们看到原本应该是0x4367_8000的数据实际存储的时候变成了00H 80H 67H 43H,这是因为计算机系统使用了小端存储。
什么是小端存储呢?
我们都知道,对于一个超过一个字节的数据,其在计算机中的存储需要跨越字节。某些机器选择在存储器中按照从最低为有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高为有效字节到到最低为有效字节的顺序存储,前一种存储方式被称为小端存储,后一种方式被称为大端存储。
举个例子,对于十六进制数0x01234567,其字节的存储顺序便依赖于机器,如下:
我们可以通过下面的函数测试是大端存储还是小端存储:
void test(void)
{
int a = 1;
unsigned char *start = &a;
if(*start == 1)
printf("小端存储");
else if(*start == 0)
printf("大端存储");
}
三、C语言知识点1
变量与值得比较
1、布尔变量与零值的比较
不可将布尔变量直接与 TRUE、 FALSE或者 1、 0进行比较 。据布尔类型的语义,零值为“ 假”(记为 FALSE),任何非零值都是“ 真”(记为TRUE)。
TRUE的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE定义为 1, 而 Visual Basic则将 TRUE定义为-1 。
假设布尔变量名字为 flag,它与零值比较的标准 if语句如下:
其它的用法都属于不良风格,例如:
2、整形变量与零值的比较
应当将整型变量用“ ==” 或“ !=” 直接与 0比较 。假设整型变量的名字为 value,它与零值比较的标准 if语句如下:
不可模仿布尔变量的风格而写成:
3、浮点变量与零值的比较
不可将浮点变量用“ ==” 或“ !=” 与任何数字比较 。千万要留意, 无论是 float还是 double类型的变量, 都有精度限制。
所以一定要避免将浮点变量用“ ==” 或“ !=” 与数字比较,应该设法转化成“ >=” 或“ <=” 形式。假设浮点变量的名字为 x,应当 将:
其中 EPSINON是允许的误差(即精度) 。
4、指针变量与零值的比较
应当将指针变量用“ ==” 或“ !=” 与 NULL比较 。指针变量的零值是“ 空”(记为 NULL)。
尽管 NULL 的值与 0相同,但是两者意义不同。假设指针变量的名字为 p,它与零值比较的标准 if语句如下:
变量及基本运算
1、整型数
如果我们确定整数非负,就应该使用unsigned int而不是int。
有些处理器处理无符号unsigned 整形数的效率远远高于有符号signed整形数(这是一种很好的做法,也有利于代码具体类型的自解释)。
因此,在一个紧密循环中,声明一个int整形变量的最好方法是:
记住,整形in的运算速度高浮点型float,并且可以被处理器直接完成运算,而不需要借助于FPU(浮点运算单元)或者浮点型运算库。
尽管这不保证编译器一定会使用到寄存器存储变量,也不能保证处理器处理能更高效处理unsigned整型,但这对于所有的编译器是通用的。
例如在一个计算包中,如果需要结果精确到小数点后两位,我们可以将其乘以100,然后尽可能晚的把它转换为浮点型数字。
2、除法和取余数
在标准处理器中,对于分子和分母,一个32位的除法需要使用20至140次循环操作。
除法函数消耗的时间包括一个常量时间加上每一位除法消耗的时间。
对于ARM处理器,这个版本需要20+4.3N次循环。这是一个消耗很大的操作,应该尽可能的避免执行。
有时,可以通过乘法表达式来替代除法。例如,假如我们知道b是正数并且b*c是个整数,那么(a/b)>c可以改写为a>(c*b)。
如果确定操作数是无符号unsigned的,使用无符号unsigned除法更好一些,因为它比有符号signed除法效率高。
3、取模的一种替代方法
我们使用取余数操作符来提供算数取模。但有时可以结合使用if语句进行取模操作。考虑如下两个例子:
优先使用if语句,而不是取余数运算符,因为if语句的执行速度更快。这里注意新版本函数只有在我们知道输入的count结余0至59时在能正确的工作。
4、使用数组下标
如果你想给一个变量设置一个代表某种意思的字符值,你可能会这样做:
5、使用别名
考虑如下的例子:
这为编译器优化代码提供了条件。
6、局部变量的类型
我们应该尽可能的不使用char和short类型的局部变量。对于char和short类型,编译器需要在每次赋值的时候将局部变量减少到8或者16位。
这对于有符号变量称之为有符号扩展,对于无符号变量称之为零扩展。这些扩展可以通过寄存器左移24或者16位,然后根据有无符号标志右移相同的位数实现,这会消耗两次计算机指令操作(无符号char类型的零扩展仅需要消耗一次计算机指令)。
可以通过使用int和unsigned int类型的局部变量来避免这样的移位操作。这对于先加载数据到局部变量,然后处理局部变量数据值这样的操作非常重要。无论输入输出数据是8位或者16位,将它们考虑为32位是值得的。
考虑下面的三个函数:
尽管结果均相同,但是第一个程序片段运行速度高于后两者。
循环语句
1、多重循环
在多重循环中, 如果有可能, 应当将最长的循环放在最内层, 最短的循环放在最外层,以减少 CPU 跨切循环层的次数。例如示例 4-4(b)的效率比示例4-4(a)的高 :
2、循环体内的判断
如果循环体内存在逻辑判断, 并且循环次数很大, 宜将逻辑判断移到循环体的外面。
示例 4-4(c)的程序比示例 4-4(d)多执行了 N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“ 流水线” 作业,使得编译器不能对循环进行优化处理, 降低了效率。
如果N非常大, 最好采用示例 4-4(d)的写法, 可以提高效率。如果 N非常小,两者效率差别并不明显,采用示例 4-4(c)的写法比较好, 因为程序更加简洁。
3、for 语句的循环控制变量
不可在 for 循环体内修改循环变量,防止 for 循环失去控制 。建议 for语句的循环控制变量的取值采用“ 半开半闭区间” 写法。
示例 4-5(a)中的 x值属于半开半闭区间“ 0 =< x < N”,起点到终点的间隔为 N,循环次数为 N。
示例 4-5(b)中的 x值属于闭区间“ 0 =< x <= N-1”,起点到终点的间隔为 N-1,循环次数为 N。
相比之下,示例 4-5(a)的写法更加直观,尽管两者的功能是相同的 。
4、更快的for()循环
这是一个简单而高效的概念。通常,我们编写for循环代码如下:
这样快的原因是因为它能更快的处理i的值–测试条件是:i是非零的吗?如果这样,递减i的值。对于上面的代码,处理器需要计算“计算i减去10,其值非负吗?
如果非负,i递增并继续”。简单的循环却有很大的不同。这样,i从9递减到0,这样的循环执行速度更快。
这里的语法有点奇怪,但确实合法的。循环中的第三条语句是可选的(无限循环可以写为for(;;))。如下代码拥有同样的效果:
这里我们需要记住的是循环必须终止于0(因此,如果在50到80之间循环,这不会起作用),并且循环计数器是递减的。使用递增循环计数器的代码不享有这种优化。
指针
我们应该尽可能的使用引用值的方式传递结构数据,也就是说使用指针,否则传递的数据会被拷贝到栈中,从而降低程序的性能。
函数通过参数接受结构数据的指针,如果我们确定不改变数据的值,我们需要将指针指向的内容定义为常量。例如:
这个示例告诉编译器函数不会改变外部参数的值(使用const修饰),并且不用在每次访问时都进行读取。
同时,确保编译器限制任何对只读结构的修改操作从而给予结构数据额外的保护。
更快的语句
在if(a>10 && b=4)这样的语句中,确保AND表达式的第一部分最可能较快的给出结果(或者最早、最快计算),这样第二部分便有可能不需要执行。
对于涉及if…else…else…这样的多条件判断,例如:
在if()语句中,如果最后一条语句命中,之前的条件都需要被测试执行一次。switch允许我们不做额外的测试。如果必须使用if…else…语句,将最可能执行的放在最前面。
四、C语言调试core dump
core dump 可以理解为当程序崩溃时,自动将内存信息保存到文件中。这里的 core 就是 memory,dump 就是将内存数据保存到磁盘的过程。
core dump 的一个常见原因是段错误(segmentation fault),这是由尝试访问非法内存位置引起的。这可能包括释放后使用、缓冲区溢出和写入空指针。
在bug很难复现的情况下,core dump 非常有用,它可以让你检查可能发生的情况。GDB 可用于读取 core dump 文件并分析程序崩溃原因。
core dump 设置
要想让自己的程序在崩溃时自动生成 core dump 文件,需要进行一些设置。
以 ubuntu 系统为例,Linux 提供了一个名为 ulimit 的程序来设置 core 文件大小和其他参数。
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7823
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7823
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
上面第一项 core file size 为 0, 表示 core 文件数量最多为 0,使用如下命令将 core 文件数量设置为无限制:
ulimit -c unlimited
然后使用 ulimit -c 命令,可以看到设置成功:
$ ulimit -c
unlimited
生成 core dump 并调试
编译代码命令:
$ gcc -ggdb -o0 <any other flags> -o file_name file_name.c
编译的时候需要加 -ggdb -o0 打开调试模式,否则打印栈帧时只能看到被调用函数的地址,而不是具体函数名和行号。
运行程序:
$ ./<file_name>
Segmentation fault (core dumped)
程序崩溃并提示 Segmentation fault,表示因为程序访问了不允许访问的内存地址,(core dumped) 表示在当前目录生成了一个文件 core,用来保存出错信息,这是一个二进制文件,需要使用 gdb 辅助分析文件内容。
使用 GDB 进行定位出错位置:
$ gdb <binary-file> <core-dump-file>
通过这条命令,就可以找到引起段错误的具体行号。
GDB 有助于在程序崩溃时检查栈帧以及变量和寄存器的状态。在这种情况下,诸如 file、where、up、down、print、info locals、info args、info registers 和 list 等命令会很有帮助。
需要记住的是,在调试 core dump 时,程序实际上并没有运行,因此与程序执行相关的命令(例如 step、next 和 continue)不可用。
实例演示
比如引起段错误的代码如下:
// core_dump.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *ptr;
*ptr = 'x';
return 0;
}
编译运行一气呵成:
$ gcc -ggdb -o0 core_dump.c -o core_dump
$ ./core_dump
Segmentation fault (core dumped)
$ ls
core core_dump core_dump.c
这时会生成一个 core 文件:
# Load program binary and core file
$ gdb core_dump core
可以看到 GDB 定位到第8八行是引起段错误的原因。
在 gdb 中,可以使用以下命令查看 backtrace(崩溃时的函数调用栈):
bt
# or (exact same command)
where
# OR (for even more details, such as seeing all arguments to the functions--
# thanks to Peter Cordes in the comments below)
bt full
# For gdb help and details, see:
help bt
# or
help where
四、C语言函数宏的封装方式
1 函数宏介绍
函数宏,即包含多条语句的宏定义,其通常为某一被频繁调用的功能的语句封装,且不想通过函数方式封装来降低额外的弹栈压栈开销。
函数宏本质上为宏,可以直接进行定义,例如:
#define INT_SWAP(a,b) \
int tmp = a; \
a = b; \
b = tmp
但上述的宏具有一个明显的缺点:当遇到 if 、 while 等语句且不使用花括号仅调用宏时,实际作用范围在宏的第一个分号后便结束。即 a = b 和 b = tmp 均不受控制语句所作用。
因此,在工程中,一般使用三种方式来对函数宏进行封装,分别为 {} 、 do{...}while(0) 和 ({}) 。下文将一一对三种方式进行分析,比较各自的优劣点。
2 { } 方式
INT_SWAP 宏使用 {} 封装后形态如下:
#define INT_SWAP(a,b)\
{ \
int tmp = a; \
a = b; \
b = tmp; \
}
此时,直接调用与在无花括号的控制语句(如 if、while)中调用均能正常运行,例如:
#define INT_SWAP(a,b) \
{ \
int tmp = a; \
a = b; \
b = tmp; \
}
int main()
{
int var_a = 1;
int var_b = 2;
INT_SWAP(var_a, var_b);
printf("var_a = %d, var_b = %d\n", var_a, var_b); // var_a = 2, var_b = 1
if (1)
INT_SWAP(var_a, var_b);
printf("var_a = %d, var_b = %d\n", var_a, var_b); // var_a = 1, var_b = 2
}
但当无花括号的 if 语句存在其他分支(else if、else 等)如:
if (1)
INT_SWAP(var_a, var_b);
else
printf("hello world!\n");
会发现编译出错:
...
/mnt/hgfs/share/pr_c/src/main.c: In function ‘main’:
/mnt/hgfs/share/pr_c/src/main.c:18:2: error: ‘else’ without a previous ‘if’
else
这是因为 INT_SWAP(var_a, var_b); 最后的 ; 已经把 if 的作用域终结了,后续的 else 当然没有找到与之匹配的 if 了。
因此,解决方法有两种,分别为不使用 ; (port.1)或规定必须使用带花括号的 if (port.2),例如:
/* port.1 */
if (1)
INT_SWAP(var_a, var_b)
else
{
printf("hello world!\n");
}
/* port.2 */
if (1)
{
INT_SWAP(var_a, var_b);
}
else
{
printf("hello world!\n");
}
可见,不使用 ; 的调用方式无论从程序阅读还是使用方法方面都是十分别扭的;而规定必须使用带花括号的 if 的调用方式有违常理的,因为宏函数应该适用于任何语法。
优缺点总结:
优点:简单粗暴。
缺点:不能在无花括号且有分支的 if 语句中直接调用;能够不带 ; 直接调用。
3 do{...}while(0) 方式
INT_SWAP 宏使用 do{...}while(0) 封装后形态如下:
#define INT_SWAP(a,b) \
do{ \
int tmp = a; \
a = b; \
b = tmp; \
}while(0)
do{...}while(0) 表示只执行一遍 {} 内的语句,表象来说与 {} 的功能是一致的。不同的是, do{...}while(0) 可以提前退出函数宏、整合为一条语句与强制调用时必须使用 ; 。
由于 do{...}while(0) 实际为 while 循环,因此可以使用关键字 break 提前结束循环。利用该特性,可以为函数宏添加参数检测。例如:
#define INT_SWAP(a,b) \
do{ \
if (a < 0 || b < 0) \
break; \
int tmp = a; \
a = b; \
b = tmp; \
}while(0)
由于 do{...}while(0); 实际为一种语法,编译器会把 do{...}while(0); 认为为一条语句。因此, do{...}while(0) 方式的函数宏可以在无花括号且有分支的 if 语句中直接调用。
例如:
#define INT_SWAP(a,b) \
do{ \
if (a < 0 || b < 0) \
break; \
int tmp = a; \
a = b; \
b = tmp; \
}while(0)
int main()
{
int var_a = 1;
int var_b = 2;
if (1)
INT_SWAP(var_a, var_b);
else
printf("hello world!\n");
printf("var_a = %d, var_b = %d\n", var_a, var_b); // var_a = 2, var_b = 1
return 0;
}
C 语言规定, do{...}while(0) 语法必须使用 ; 作为语句结尾。因此不可能存在以下语句的程序出现:
if (1)
INT_SWAP(var_a, var_b)
else
{
printf("hello world!\n");
}
优缺点总结:
优点:支持在无花括号且有分支的 if 语句中直接调用;支持提前退出函数宏;强制调用时必须使用。
缺点:无返回值,不能作为表达式的右值使用。
4 ({ }) 方式
({}) 为 GNU C 扩展的语法,非 C 语言的原生语法。
INT_SWAP 宏使用 ({}) 封装后形态如下:
#define INT_SWAP(a,b) \
({ \
int tmp = a; \
a = b; \
b = tmp; \
})
与 do{...}while(0) 相同, ({}) 支持在无花括号且有分支的 if 语句中直接调用。例如:
#define INT_SWAP(a,b) \
({ \
int tmp = a; \
a = b; \
b = tmp; \
})
int main()
{
int var_a = 1;
int var_b = 2;
if (1)
INT_SWAP(var_a, var_b);
else
printf("hello world!\n");
printf("var_a = %d, var_b = %d\n", var_a, var_b); // var_a = 2, var_b = 1
return 0;
}
与 do{...}while(0) 不同的是, ({}) 不能提前退出函数宏与支持返回值。 ({}) 毕竟不是 while 循环,不能直接使用 break 退出函数宏是比较容易理解。那支持返回值是什么意思呢?
答案是 C 语言规定 ({}) 中的最后一条语句的结果为该双括号体的返回值。
例如:
int main()
{
int a = ({
10;
1000;
});
printf("a = %d\n", a); // a = 1000
}
因此, ({}) 可以为函数宏提供返回值。
例如:
#define INT_SWAP(a,b) \
({ \
int ret = 0; \
if (a < 0 || b < 0) \
{ \
ret = -1; \
} \
else \
{ \
int tmp = a; \
a = b; \
b = tmp; \
} \
ret; \
})
int main()
{
int var_a = 1;
int var_b = 2;
if (INT_SWAP(var_a, var_b) != -1)
printf("swap success !!\n"); // swap success !!
else
printf("swap fail !!\n");
printf("var_a = %d, var_b = %d\n", var_a, var_b); // var_a = 2, var_b = 1
return 0;
}
可见,此时的 INT_SWAP 宏已与函数十分接近。
优缺点总结:
优点:支持在无花括号且有分支的 if 语句中直接调用;有返回值,支持作为表达式的右值。
缺点:不支持提前退出函数宏;非 C 的原生语法,编译器可能不支持。
5 总结
综上,在 {} 、 do{...}while(0) 和 ({}) 这三种函数宏的封装方式之中,应尽可能不使用 {} ,考虑兼容性一般选择使用 do{...}while(0) ,当需要函数宏返回时可以考虑使用 ({}) 或直接定义函数。
五、正确使用动态内存
常见错误与预防
1 分配后忘记释放内存
void func(void)
{
p = malloc(len);
do_something(p);
return; /*错误!退出程序时没有释放内存*/
}
预防:编写代码时malloc()和free()保证成对出现,避免忘记资源回收。
int func(void)
{
p = malloc(len);
if (condition)
return -1; /*错误!退出程序时没有释放内存*/
free(p);
return 0;
}
预防:一旦使用动态内存分配,请仔细检查程序的退出分支是否已经释放该动态内存。
2 释放内存调用错误指针
void func(void)
{
p = malloc(len);
val = *p++; /*错误!动态内存句柄不可移动*/
free(p);
}
预防:千万不要修改动态内存句柄!可以另外赋值给其他指针变量,再对该动态内存进行访问操作。
3 分配内存不够导致溢出
void func(void)
{
len = strlen(str);
p = malloc(len);
strcpy(p, str); /*错误!str的’\0’写到动态内存外*/
}
预防:分配内存前仔细思考长度是否足够,千万注意字符串拷贝占用内存比字符串长度大1。
自动查错机制
尽管在开发过程中坚守原则和谨慎编程甚至严格测试,然而内存泄露的错误还是难以杜绝,如何让系统自动查出内存泄露的错误呢?
一种比较好的方法是建立日志块,即每次分配内存时记录该内存块的指针和大小,释放时再去除该日志块,如果有内存泄露就会有对应的日志块记录这些内存没有释放,这样就可以提醒程序员进行查错。
有了上述日志块操作函数,再来实现动态内存分配与释放函数就很容易了。只有当处于DEBUG版本和打开内存调试DMEM_DBG时才进行日志登录,否则MallocExt()和FreeExt()函数与malloc()和free()是等价的,这样保证了系统处于发布版本时的性能。
代码已经过严格测试,但这不是盈利的商业代码,即没有版权。但如果因代码错误带来的任何损失作者具有免责权利。
代码部分:
首先定义日志块结构体:
/* Log of dynamic memory usage */
typedef struct _dmem_log
{
struct _dmem_log *p_stNext; /* Point to next log */
const void *p_vDMem; /* Point to allocated memory by this pointer */
INT32S iSize; /* Size of the allocated memory */
} DMEM_LOG;
然后为该结构体开辟内存:
static DMEM_LOG *s_pstFreeLog; /* Point to free log pool by this pointer */
static INT8U s_byNumUsedLog;
static DMEM_LOG *s_pstHeadLog; /* Point to used log chain by this pointer */
/* Pool of dynamic memory log */
#define NUM_DMEM_LOG 20
static DMEM_LOG s_astDMemLog[NUM_DMEM_LOG];
下面是内存日志块的操作函数:初始化、插入日志和移除日志:
/********************************************************** * Initialize DMem Log
* Description : Initialize log of dynamic memory
* Arguments : void
* Returns : void
* Notes :
**********************************************************/
static void InitDMemLog(void)
{
INT16S nCnt;
/* Initialize pool of log */
for (nCnt = 0; nCnt < NUM_DMEM_LOG; ++nCnt)
{
/* Point to next one */
s_astDMemLog[nCnt].p_stNext = &s_astDMemLog[nCnt + 1];
}
s_astDMemLog[NUM_DMEM_LOG - 1].p_stNext = NULL;
s_pstFreeLog = &s_astDMemLog[0]; /* Point to the 1th log */
return;
}
/********************************************************** * Log DMem
* Description : Join an allocated memory into log pool
* Arguments : const void *p_vAddr point to address of this allocated memory by this pointer
* INT32S iSize size of this allocated memory
* Returns : void
* Notes :
**********************************************************/
static void LogDMem(const void *p_vAddr, INT32S iSize)
{
ASSERT(p_vAddr && iSize > 0);
DMEM_LOG *p_stLog;
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
/* Get a log from free pool */
OS_ENTER_CRITICAL(); /* Avoid race condition on s_pstFreeLog */
if (!s_pstFreeLog)
{
OS_EXIT_CRITICAL();
PRINTF("Allocate DMemLog failed.\r\n");
return;
}
p_stLog = s_pstFreeLog;
s_pstFreeLog = s_pstFreeLog->p_stNext;
OS_EXIT_CRITICAL();
/* Don't need to protect this log that is free one currently */
p_stLog->p_vDMem = p_vAddr;
p_stLog->iSize = iSize;
/* Put this log into used chain */
OS_ENTER_CRITICAL(); /* Avoid race condition */
p_stLog->p_stNext = s_pstHeadLog;
s_pstHeadLog = p_stLog;
++s_byNumUsedLog;
OS_EXIT_CRITICAL();
return;
}
/********************************************************** * Unlog DMem
* Description : Remove an allocated memory from log pool
* Arguments : const void *p_vAddr point to address of this allocated memory by this pointer
* Returns : void
* Notes :
**********************************************************/
static void UnlogDMem(const void *p_vAddr)
{
ASSERT(p_vAddr);
DMEM_LOG *p_stLog, *p_stPrev;
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
/* Search the log */
OS_ENTER_CRITICAL(); /*Avoid race condition */
p_stLog = p_stPrev = s_pstHeadLog;
while (p_stLog)
{
if (p_vAddr == p_stLog->p_vDMem)
{
break; /* Have found */
}
p_stPrev = p_stLog;
p_stLog = p_stLog->p_stNext; /* Move to next one */
}
if (!p_stLog)
{
OS_EXIT_CRITICAL();
PRINTF("Search Log failed.\r\n");
return;
}
/* Remove from used pool */
if (p_stLog == s_pstHeadLog)
{
s_pstHeadLog = s_pstHeadLog->p_stNext;
}
else
{
p_stPrev->p_stNext = p_stLog->p_stNext;
}
--s_byNumUsedLog;
OS_EXIT_CRITICAL();
/* Don't need to protect this log that is free one currently */
p_stLog->p_vDMem = NULL;
p_stLog->iSize = 0;
/* Add into free pool */
OS_ENTER_CRITICAL(); /* Avoid race condition */
p_stLog->p_stNext = s_pstFreeLog;
s_pstFreeLog = p_stLog;
OS_EXIT_CRITICAL();
return;
}
带日志记录功能的内存分配MallocExt()和内存释放FreeExt()函数:
/*********************************************************
* Malloc Extension
* Description : Malloc a block of memory and log it if need
* Arguments : INT32S iSize size of desired allocate memory
* Returns: void *NULL= failed, otherwise=pointer of allocated memory
* Notes :
**********************************************************/
void *MallocExt(INT32S iSize)
{
ASSERT(iSize > 0);
void *p_vAddr;
p_vAddr = malloc(iSize);
if (!p_vAddr)
{
PRINTF("malloc failed at %s line %d.\r\n", __FILE__, __LINE__);
}
else
{
#if (DMEM_DBG && DBG_VER)
memset(p_vAddr, 0xA3, iSize); /* Fill gargage for debug */
LogDMem(p_vAddr, iSize); /* Log memory for debug */
#endif
}
return p_vAddr;
}
/**********************************************************
* Free Extension
* Description : Free a block of memory and unlog it if need
* Arguments : void * p_vMem point to the memory by this pointer
* Returns : void
* Notes :
**********************************************************/
void FreeExt(void *p_vMem)
{
ASSERT(p_vMem);
free(p_vMem);
#if (DMEM_DBG && DBG_VER)
UnlogDMem(p_vMem); /* Remove memory from log */
#endif
return;
}