文章目录
前言
在VS编译中,Debug模式下会生成.pdb文件和.exe文件,DPB文件主要存储了VS调试程序时所需要的基本信息,主要包括源文件名、变量名、函数名、FPO(帧指针)、对应的行号等等。
 而本篇文章主要讲代码编译后生成.exe文件的过程
 在ANSI C的任何一种实现中,存在两个环境:
- 翻译环境,在整个环境中源代码被转换为可执行的机器指令
 - 执行环境,用于实际执行代码
 
一、详解编译+链接
1.1 翻译环境


- 组成一个程序的每个源文件通过编译分别转换为目标代码(object code)
 - 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
 - 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
 
1.2 编译
编译也分为好几个阶段:预编译、编译、汇编、链接(以下都是在gcc编译器中)
- 预编译:将包含的头文件展开,删除代码中的注释,进行符号的替换和宏替换。执行
gcc -E指令,会生成xxx.i文件,可以打开看到预编译处理之后代码 - 编译:把C语言代码编译成汇编代码,对代码进行词法分析、语法分析、语义分析、符号汇总(汇总的都是全局符号,而局部符号是不进行汇总的)。执行
gcc -S指令,会生成xxx.s文件,可以看到编译之后的汇编代码 - 汇编:将汇编代码转换为机器指令并形成符号表。执行
gcc -c指令会生成xxx.o文件。在Linux系统下,test.o的二进制文件是以elf的格式来组织文件的。在Linux平台下我们可以用readelf翻译并查看它的内容 - 链接:合并段表(就是将对应的段合并起来)、符号表的合并和符号表的重定位,链接会将二进制指令目标文件
test.o等链接在一起形成可执行的程序test.out。符号表的合并,就是将各自的符号表合并到一起,比如说:test.o中Add的无效地址,需把add.o中Add的地址与之合并再重新定位到变量的真实地址,这样才是有意义的。 
总结: 编译期间的符号汇总,到汇编阶段的形成符号表,再到链接时的合并段表和符号表的合并和重新定位,都是为了最后生成可执行程序时能够找到并链接各个源文件文件中的符号(全局变量或者函数)
1.3 运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
 - 程序的执行便开始,接着便调用main函数
 - 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
 - 终止程序。正常终止main函数;也有可能是意外终止
 
二、预处理详解
2.1 预定义符号
  __FILE__       //进行编译的源文件
  __LINE__       //文件当前的行号
  __DATE__       //文件被编译的日期
  __TIME__       //文件被编译的时间
  __STDC__      //如果编译器遵循ANSI C,值为1,否则未定义
 
这些预定义符号都是语言内置的
printf("file:%s line:%d\n", __FILE__, __LINE__);
 
2.2 #define定义标识符
 #define MAX 100
 #define CASE break;case
 #define reg register 
 
2.3 #define定义宏
宏的申明方式:
#define name( parament-list ) stuff
 
其中的parament-list 是一个由逗号隔开的符号表,可能出现在stuff中
注: 参数列表的左括号必须与name紧邻;如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#define SQUARE(x) ((x)*(x))
 
2.4 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
 - 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
 - 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
 
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
 - 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
 
2.5 #和##
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
 

 可以发现字符串是有自动连接的特点的
#的作用:
int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);/
 
代码中的#VALUE会预处理器处理为:
 “VALUE”
 

##的作用:
#define ADD_TO_SUM(num, value) \
 sum##num += value;
//...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
 
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
2.6 宏与函数对比
宏的优点:
- 宏比函数在程序的规模和速度方面更胜一筹
 - 宏是类型无关的
 
宏的缺点:
- 宏定义比较长时,可能会大幅度增加程序的长度
 - 宏无法调试
 - 宏由于类型无关,不够严谨
 - 可能会带来运算符优先级的问题,导致程序容易出错
 
2.7 #undef
这条指令用于移除一个宏定义
 #undef NAME
 //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
 
2.8 条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
常见的条件编译指令:
1.
#if 常量表达式
   //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
	#ifdef OPTION1
 		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif
 
三、文件包含
#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样
这种替换的方式很简单:
 预处理器先删除这条指令,并用包含文件的内容替换。
 这样一个源文件被包含10次,那就实际被编译10次。
3.1 头文件被包含的方式:
- 本地文件包含
 
 #include "filename"
 
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
Linux环境的标准头文件的路径:
   /usr/include
 
VS环境的标准头文件的路径:
C:\Program Files(x86)\Microsoft Visual Studio 12.0\VC\include
 
- 库文件包含
 
#include <filename.h>
 
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
 库文件也可以使用 “” 的形式包含
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
3.2 嵌套文件包含
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
条件编译可以解决这个问题
 每个头文件的开头写:
 #ifndef __TEST_H__
 #define __TEST_H__
 //头文件的内容
 #endif 
 
或者
#pragma once
 
就可以避免头文件的重复引入
四、其他预处理指令
#error   //在编译期间产生错误信息,并阻止程序编译
#pragma  
#line   //#line可以修改当前源文件中的宏的值,__FILE__和__LINE__
#pragma pack() //设置结构体的对齐数
 
int main()
{
	printf("%s,line:%d \n", __FILE__, __LINE__);
#line 2 "test.c"
	printf("%s,line:%d \n", __FILE__, __LINE__);
#line 10 "test1.c"
	printf("%s,line:%d \n", __FILE__, __LINE__);
	return 0;
}
 









