高唐做网站推广,网页制作与网站建设技术大全 pdf,如何 建设一个网站,wordpress写文章排版操作系统系列#xff1a;快速了解C语言的编译 关于C语言的编译编译时会发生什么#xff1f;C预处理器实际的编译汇编链接 传递参数到程序中匈牙利命名法系统调用Unix系统调用 关于C语言的编译
开发者写好一段代码后#xff0c;需要将编码语言转换为设备认识的机器语言才能执… 操作系统系列快速了解C语言的编译 关于C语言的编译编译时会发生什么C预处理器实际的编译汇编链接 传递参数到程序中匈牙利命名法系统调用Unix系统调用 关于C语言的编译
开发者写好一段代码后需要将编码语言转换为设备认识的机器语言才能执行也就是说将C语言文件转化为可执行文件这个过程称为编译。
编译时会发生什么
编译 C 程序时会发生什么呢值得我们花一些时间来看一看。 下面这个示例来自于 Unix 编译器但原理适用于任何 C 编译器。创建可执行文件的过程至少涉及四个单独的步骤预处理输入、进入实际的编译工作来生成汇编程序文件、汇编该文件以创建目标文件以及链接这些目标文件来创建可执行文件。
C预处理器
预处理器是一个以包含预处理指令的C程序作为输入的程序它会展开这些指令预处理器的输出是带有这些展开的预处理指令的C程序。预处理指令有时被称为宏,尤其是在汇编中。宏展开总是使用字符串替换的形式。 C预处理器指令由每一行的第一列的**#**字符指示。 你很熟悉的一条指令是
#include这条指令后面紧跟着一个文件名通常文件带有后缀名.h并且文件的内容会被拷贝到输出文件。文件名可以用 或者 “包起来如果文件名用 索引那么预处理器会在路径/usr/include路径查找它或者某些由管理员设置的其他路径。如果文件用” 索引那么预处理器会在当前路径中查找或者将其作为绝对路径名来跟踪。 另一个简单的预处理器指令是
#define它通常有两个参数预处理器会简单的用第二个字符串替换第一个字符串。比如说
#define BUFSIZE 1024预处理器会替换输入文件中所有使用BUFSIZE的实例在输出文件中用1024代替。比如说
char buffer[BUFSIZE]要被替换为
char buffer[1024];注意这里有个常见的错误是在第二个字符串的结尾处放了个 ; 那么会导致很难检查到的编译错误比如
#define BUFSIZE 1024;会导致输出字符串是
char buffer[1024;];在句法上这是错误的。
也有可能写一些使用参数的宏定义的表达式比如
#define SQUARE(X) X * X如果有一行类似于这样的文本
n SQUARE(m);那么它将会被扩展为
n m * m;这里有一个再复杂一点的例子
#define SWAP(TYPE,M,N) {TYPE temp; tempM; MN; N temp;}那么文本中的这一行
SWAP(int,a,b)会被展开为
{int temp; tempa; ab; btemp;}要搞清楚一点这个过程没有发生过实际的处理或者说预处理期间甚至不检查语法是否正确。预处理器只是简单的把一个字符串替换为另一个。 预处理器可以定义变量而不用去设置实际的值。比如
#define _MYHEADER_H_这种方式通常用于控制条件编译是预处理器的另一个特性。条件编译意味着只有当某些变量被定义或者未被定义时某些代码才会被编译相应地预处理器关键字#ifdef如果变量被定义和#ifndef如果变量未被定义和#endif搭配使用。例如
#ifndef _MYHEADER_H_
#define _MYHEADER_H_
/*
code for my header.h, which will only be compiled if _MYHEADER.H had not been previously defined.
*/也可以通过gcc命令行来定义变量同样地要使用-D选项比如
gcc -D __sparc__ -o outfile infile.c你稍后可以在infile.c文件中输入如下代码
#ifdef __sparc__
/* code to be compiled for sparc,but not for other architectures. */
#endifC预处理器是一个cpp输入是带有预处理指令的文件输出是所有预处理指令都已经展开的文件。这个文件会通过一个带有后缀.i的临时文件名给出临时文件使用后会自动删除但是开发者可以在这一步后通过传递-E标志给gcc来停止这个操作。输出文件会写道标准输出所以如果开发者想看一看预处理器做了什么那么可以将它重定向到一个文件。比如
gcc -E myfile.c myfile.i下面的程序经过C预处理器但是不进入编译器输出是什么下面给出一个关于预处理器的小测验。
#define BUFSIZE 1024
#define putchar(x) putc(x,stdout)
#define NULL (void *0)
#define DWORD unsigned long int
#define LOBYTE(X) X 0x0F
#define SUM(x,y) x yint main()
{int a,b,c;DWORD d;char buffer[BUFSIZE];char *s;a 5000;s NULL;b LOBYTE(a);d SUM(a,b);putchar(e);return 0;
}实际的编译
C编译器被称为cc1它的输入是预处理器输出的文件换句话说一个纯C文件。它的输出是一个汇编文件在Solaris上汇编文件带有后缀.s如果开发者想要查看以下这个汇编文件你可以通过传递-S标志给gcc在汇编之前停止这个操作。否则汇编过程之后.s文件会被删掉。
汇编
gcc脚本紧接着召唤主机上的汇编器大多数的Unix机器上自带的汇编器是as并且GNU相当于是gas输入是编译器生成的汇编文件输出是一个带有后缀.o的对象文件(在Windows上对象文件的后缀是.obj)。 开发者可以在对象文件创建后但是在实际的可执行文件创建以前通过传输-c标志到gcc来停止这个过程。 如果输入文件不止一个那么在进入下一步之前上述过程预处理编译汇编要对每一个输入文件重复一遍。
链接
假设我们有2个这样的源文件 第一个文件是file1.c
/* file1.c */
#include stdio.h
int g; /* a gloal variable */
extern double dg; /* another global var, defined in some other file */
void fctnOne(); /* a function prototype */int main()
{int x; /* a variable local to main (an automatic variable) */x 3;dg 3.14;g 17;fctnOne();printf(x is %d, g is %d, dg is %f \n,x,g,dg);return 0;
}
void fctnTwo()
{int x;x 5;g 11;dg dg * 2;
}第二个文件 file2.c
/* file2.c */
extern int g;
double dg;
void fctnTwo(); /* function prototype */
void fctnOne()
{int x 44;g x;dg dg 2;fctnTwo();
}如果编译行是这个样子的 gcc -g -Wall file1.c file2.c 对这两个源文件进行预处理编译和汇编应该会产生2个对象文件称为file1.o和file2.o不管如何这两个对象文件都有未被解析的引用也就是说在其它文件中定义的变量和函数。当file1.o被生成的时候它会包含名为fctnOne的函数调用但是在汇编时它并不知道这个函数的地址。同样地有一个对变量dg的引用汇编器并不知道这个变量的地址。还有一个printf调用也没在这个文件中定义。文件file2.o也有未被解析的引用fctnTwo和g。链接器的工作是解析所有这些未被解析的引用完成这件事需要使用2个依赖对象文件的表。一个表是definition table列出了这个文件中定义的所有全局函数和变量以及相应的地址。另一个表是use table列出了每个被使用的未定义的变量和函数的实例。 这里有4个表供这2个文件使用。
用于file1.c的definition table 用于file1.c的use table
g gd (line 13)
main() fctnOne() (line 15)
fctnTwo() printf() (line 16)dg (line 17)dg (line 26, first instance)dg (line 26, second intance)
Definition table for file2.c Use table for file2.c
gd g (line 9)
fctnOne() fctnTwo() (line 11)链接器通常通过2个pass来完成它的工作首先遍历所有的定义表构建一个包含所有函数和变量的全局定义表这些函数和变量与它们的地址一起定义在任何文件中然后再次遍历所有的文件用真实地址替换使用列表中的所有未解析的引用。 最终链接器搜索库去解析更多未被解析的引用。链接器通常配置为自动搜索标准C库libc它包含像是printf函数的代码。开发者可以告诉链接器搜索其它库使用-l标志该标志可以传递给gcc。比如如果开发者使用math库中的函数可以通过-lm标志来告诉链接器。 更常见的是编译器使用动态链接去链接库函数。使用静态链接的话用于可执行程序的库是直接作为可执行程序的一部分的。使用动态链接那么库符号的名字存储在可执行程序中程序运行时如果需要调用库函数那么操作系统会在执行前为可执行程序查找代码的位置。动态链接的优势是对于经常使用的库函数比如printf只有一个代码实例静态链接的情况用于printf的代码拷贝到每个使用它的可执行程序中。动态链接的缺点是需要一点运行时间因为每次调用时系统都需要花点时间去查找库函数的地址。 链接器合并了所有输入到一个可执行程序镜像某些情况下这可能需要解决在某些模块或者全部模块中的地址的再分配。 下面时2个分别编译的程序并且由链接器链接到一起创建一个可执行文件。展示用于One.o和Two.o One.c的定义表和使用表的内容。
extern int a(int);
extern int b;
int c;
int d(int x)
{int y;y x * 2;return y;
}
int main()
{int e;c 7;b a(c);e d(b);printf(%d\n,b);return 0;
}Two.c
int b;
int d(int);
int a(int x)
{int z;z d(x);return z;
}传递参数到程序中
当开发者从命令行运行程序的时候可以传递参数到程序中。函数main()可以访问这些参数。main默认由2个参数int argc和char *argv[]变量argc会自动设置为在运行时需要包含的所有参数数目包括可执行程序自己变量argv参数向量时一个指针数组指向字符串。数组的尺寸时argc这里有一个简短的程序展示了它的参数。
#include stdio.h
int main(int argc, char *argv[])
{int i;for(i 0;iargc;i){printf(%s\n,argv[i]);}return 0;
}假设这是一个命令行
a.out first second third那么输出应该是
a.out
first
second
third那么argc的值应该是4.
在Windows上编译C或者C程序 在windows机器上开发者将会使用Microsoft.NET编译器如果没有合适的访问Microsoft 编译器那么可以使用任何免费的编译器总之编译器要可以访问WIN32 APIs.
匈牙利命名法
如果开发者阅读Microsoft C/C文档或者Microsoft的样例代码需要理解匈牙利命名法。这是一种Microsoft惯常使用的变量命名方法。例如描述函数ReadFile的在线帮助就是这种命名方式。
BOOL ReadFile(HANDLE hFile,LPVOID lpBuffer,DWORD nNumberOfBytesToRead,LPDWORD lpNumberOfBytesRead,LPOVERLAPPED lpOverlapped
);匈牙利命名发是由Charles Simonyi 开发的他是最初的Microsoft软件总架构师他可能有过匈牙利血统它是一个命名惯例允许编程人员确定类型并且使用标识符变量函数常量等。 变量名字包含可选的前缀指示变量类型的标签以及变量名。前缀是小写字母变量名自身用大写字母开头。 通用的标签有
FlagTypeExamplebBooleanbGameOverch or csingle charchGradedwdouble word(32 bits)dwBytesReadnintegernStringLengthddouble precision realszLastNamesznull terminated char stringszLastNameppointerpBufferlplong pointerlpBufferCClass NameCWidget另外多数Microsoft的样例代码都包含头文件windows.h它定义了大多数的数据类型开发者不需要去看变量类型int在Microsoft代码中的定义按惯例所有这些都是大写的这里有一些例子DWORD unsigned long(代表双字)WORD unsigned short(16比特)BOOL booleanBYTE unsigned charLPDWORD pointer to a DWORDLPVOID pointer to type void通用指针LPCTSTR pointer to a const string
系统调用
现在的操作系统可以运行在2个或多个模式典型的称为用户模式和内核模式。编译和运行普通程序的普通用户在用户模式意味着程序只可以访问它自己的内存区域。然而多数用户程序需要访问内核提供的服务。一个显著的例子是从文件中进行读取。通常用户是不允许访问读取独立块的底层代码的需要某些机制能允许用户程序去访问这些服务。这些对内核服务的调用就被称为Unix的系统调用和应用程序接口或者Microsft的APIs.
Unix系统调用
大概有190个Unix系统调用对于每种系统这个数字可能略有不同这些调用总是使用C函数调用的格式也就是说它们可以使用普通变量做参数也可以使用指向普通变量的指针而且它们会返回一个值。这个返回值通常是int型的。一般来说返回值是正数或者0表示调用成功而负返回值表示系统调用由于某些原因失败了。如果系统调用失败全局的external int变量errno会被设置程序可以查看该值来确认系统调用失败的原因。 尽管errno是个整数它的值都是有符号名称的可以查看每个系统调用的man pages来确认这些值的意义。例如打开文件的系统调用open成功的话它会返回可以给其它系统调用访问这个文件的文件描述符然而总是有一些原因使打开文件的尝试失败这里展示了一部分open的在线man page
ERRORSThe open() function will fail if:EACCESSearch permission is denied on a component of the pathprefix, or the file exists and the permissions speci-fied by oflag are denied, or the file does not existand write permission is denied for the parent direc-tory of the file to be created, or O_TRUNC is speci-fied and write permission is denied.EDQUOTThe file does not exist, O_CREAT is specified, andeither the directory where the new file entry is beingplaced cannot be extended because the users quota ofdisk blocks on that file system has been exhausted, orthe users quota of inodes on the file system wherethe file is being created has been exhausted.EEXISTThe O_CREAT and O_EXCL flags are set, and the namedfile exists.EINTR A signal was caught during open().EFAULTThe path argument points to an illegal address.EISDIRThe named file is a directory and oflag includesO_WRONLY or O_RDWR.EMFILEOPEN_MAX file descriptors are currently open in thecalling process.ENFILEThe maximum allowable number of files is currentlyopen in the system.每个可能的错误条件都有符号名称所有符号名称都以大写的字母E开头定义在头文件中。变量errno在失败时设置可以通过写代码发现导致错误的原因这里有一个实现了这件事的C程序架构。 … #include errno.h extern int errno; … int main() … int returnval … returnval open(…) if(returnval 0) /* the open failed */ switch(errno){ case EACCES:… case EDQUOT:… … … } 良好的程序总是会检查可能导致失败的系统调用的返回值并且在失败事件中执行合适的操作。对于所有开发人员来说都要这样做。 Unix有一个库函数void perror(const char *msg)它可以向开发者提供带有标准错误消息的信息基于errno的值。 这里有一段代码段展示它要怎么用。
int fd;
fd open(...)
if(fd 0) perror(Error opening file)如果因为没有文件名导致调用open失败那么这个消息可能会展示在终端上
Error opening file: No such file or directory也有一个函数char *strerror(int errno) 确保包含了头文件string.h它返回一个对应错误码参数的字符串。