操作系统系列:快速了解C语言的编译

操作系统系列:快速了解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; temp=M; M=N; N = temp;}

那么文本中的这一行,

SWAP(int,a,b)

会被展开为,

{int temp; temp=a; a=b; b=temp;}

要搞清楚一点,这个过程没有发生过实际的处理,或者说预处理期间甚至不检查语法是否正确。预处理器只是简单的把一个字符串替换为另一个。
预处理器可以定义变量,而不用去设置实际的值。比如:

#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. */
#endif

C预处理器是一个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;i<argc;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软件总架构师(他可能有过匈牙利血统),它是一个命名惯例,允许编程人员确定类型并且使用标识符(变量,函数,常量等)。
变量名字包含可选的前缀,指示变量类型的标签,以及变量名。前缀是小写字母,变量名自身用大写字母开头。
通用的标签有:

FlagTypeExample
bBooleanbGameOver
ch or csingle charchGrade
dwdouble word(32 bits)dwBytesRead
nintegernStringLength
ddouble precision realszLastName
sznull terminated char stringszLastName
ppointerpBuffer
lplong pointerlpBuffer
CClass NameCWidget
另外,多数Microsoft的样例代码都包含头文件<windows.h>,它定义了大多数的数据类型,开发者不需要去看变量类型int在Microsoft代码中的定义,按惯例,所有这些都是大写的,这里有一些例子:
DWORD unsigned long(代表双字)
WORD unsigned short(16比特)
BOOL boolean
BYTE unsigned char
LPDWORD pointer to a DWORD
LPVOID 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 user's quota  ofdisk blocks on that file system has been exhausted, orthe user's 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),它返回一个对应错误码参数的字符串。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/226606.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jmeter分布式性能测试,80%资深测试都会遇到这个坑!

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

深度学习项目部署:解析 NVIDIA Docker 中的 CUDA 镜像版本:base 版本、 runtime 版本、devel 版本

近期&#xff0c;我不得不将深度学习项目部署到 Docker 环境中&#xff0c;而在这个过程中避免不了涉足 NVIDIA Docker 的坑。尽管确认其为非常实用的工具&#xff0c;但容器里却一片干净&#xff0c;什么都没有&#xff0c;需要一些时间去进行配置。在这篇博客中&#xff0c;我…

【C语言】动态内存管理(C语言的难点与精华,数据结构的前置知识,你真的掌握了吗?)

文章目录 引言一、为什么要动态内存分配二、动态内存分配的相关函数2.1 malloc2.2 free2.3 calloc2.4 realloc 三、常见的动态内存的错误3.1 对NULL指针的解引用3.2 对动态内存越界访问3.3 对非动态内存释放3.4 对动态内存部分释放3.5 对动态内存多次释放3.6 未对动态内存释放&…

v-md-editor高级使用之自定义目录

​ 官方给出的目录设置参见&#xff1a;https://code-farmer-i.github.io/vue-markdown-editor/zh/senior/toc.html#%E7%9B%B8%E5%85%B3%E9%85%8D%E7%BD%AE ​ 在做实际使用中往往可能需要将目录结构独立出来&#xff0c;经过近一天的研究终于明白其实现逻辑&#xff0c;并将目…

C++知识点总结(9):前缀和

一、前缀和 1. 意义 数列1520305065下标12345前缀和153565115180 2. 表示 前缀和&#xff1a;用数组表示&#xff0c;因为每一项的前缀和都能算出来。 3. 公式 值前缀和数组 a [ 1 ] a[1] a[1] s [ 1 ] s[1] s[1] s [ 1 ] a [ 2 ] s[1]a[2] s[1]a[2] s [ 2 ] s[2] s[2] …

3.qml 3D-Node类学习

Node类是在View3D 中的对象基础组件&#xff0c;用于表示3D空间中的对象&#xff0c;类似于Qt Quick 2D场景中的Item&#xff0c;介绍如下所示&#xff1a; 如上图可以看到&#xff0c;Node类的子类非常多&#xff0c;比如Model类(显示3D模型)、ParticleSystem3D粒子系统类、Li…

苹果计划将全球1/4的IPhone产能转移至印度

KlipC报道&#xff1a;据相关人士报道&#xff0c;苹果希望在未来2到3年内每年在印度生产超过5000万部iphone&#xff0c;要是该计划得以实现&#xff0c;印度将占领全球iPhone产量的四分之一。 KlipC的分析师Alex Su表示&#xff1a;“此次iPhone15推出是苹果印度制造计划的一…

认知能力测验,①如何破解数字推理类测试题?

校园招聘&#xff08;秋招春招&#xff09;&#xff0c;最为常见的认知能力测验&#xff0c;在线工具网将整理分析关于认知能力测验的系列文章&#xff0c;希望能帮助大家顺利通过认知能力测评&#xff0c;找到自己心仪的工作。 数字推理测试&#xff0c;是我们在求职中经常会…

C# 获取Windows所有窗口句柄

写在前面 在做录屏或截屏操作时&#xff0c;需要获取当前正在运行中的桌面程序句柄&#xff0c;在网上查找资源的的时候&#xff0c;发现了一个工具类还不错&#xff0c;这边做个验证记录。 参考代码 public class WindowApi{//寻找目标进程窗口 [DllImport("USER…

【大数据】Hudi 核心知识点详解(二)

&#x1f60a; 如果您觉得这篇文章有用 ✔️ 的话&#xff0c;请给博主一个一键三连 &#x1f680;&#x1f680;&#x1f680; 吧 &#xff08;点赞 &#x1f9e1;、关注 &#x1f49b;、收藏 &#x1f49a;&#xff09;&#xff01;&#xff01;&#xff01;您的支持 &#x…

商用机器人,不好用是原罪

热潮褪去后&#xff0c;所有的问题都汇总成一个词&#xff0c;不好用。 从炙手可热到“大玩具” 一款产品好用与否&#xff0c;更多时候人们不会关心它先进的技术、工艺、用料&#xff0c;也不会考虑所谓的潮流趋势或前景&#xff0c;只会用最朴素的直观感受告诉你&#xff0…

【Redis】Redis.conf详解

Redis.conf详解 启动的时候&#xff0c;就通过配置文件来启动&#xff01; 工作中&#xff0c;一些小小的配置&#xff0c;可以让你脱颖而出&#xff01; 单位 配置文件 unit单位 对大小写不敏感&#xff01;include包含其他配置文件 就是好比我们学习Spring、Improt&#x…

讨论用于评估DREX的五种DR指标

概要 动态范围是已经使用了近一个世纪的用于评估接收机性能的参数。这里介绍五种动态有关指标的定义及测试方法&#xff0c;用于评估数字接收激励器&#xff08;DREX&#xff0c;digital receiver exciters&#xff09;。DREX是构成雷达的关键整部件&#xff0c;其瞬时带宽&am…

LeetCode经典150题Golang版.189. 轮转数组

题目 189. 轮转数组 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转…

docker consul 容器的自动发现与注册

consul相关知识 什么是注册与发现 服务注册与发现是微服务架构中不可或缺的重要组件。起初服务都是单节点的&#xff0c;不保障高可用性&#xff0c;也不考虑服务的压力承载&#xff0c;服务之间调用单纯的通过接口访问。直到后来出现了多个节点的分布式架构&#xff0c;起初的…

团体程序设计天梯赛练习集(L1-016-L1-025)

L1-016 查验身份证 一个合法的身份证号码由17位地区、日期编号和顺序编号加1位校验码组成。校验码的计算规则如下&#xff1a; 首先对前17位数字加权求和&#xff0c;权重分配为&#xff1a;{7&#xff0c;9&#xff0c;10&#xff0c;5&#xff0c;8&#xff0c;4&#xff0c;…

kafka配置多个消费者groupid kafka多个消费者消费同一个partition(java)

目录 1- 单播模式&#xff0c;只有一个消费者组2- 广播模式&#xff0c;多个消费者组3- Java实践 kafka是由Apache软件基金会开发的一个开源流处理平台。kafka是一种高吞吐量的分布式发布订阅消息系统&#xff0c;它可以处理消费者在网站中的所有动作流数据。 kafka中partition…

Git忽略已经提交的文件

原理类似于 Android修改submodule的lib包名

一文搞懂OSI参考模型与TCP/IP

OSI参考模型与TCP/IP 1. OSI参考模型1.1 概念1.2 数据传输过程 2. TCP/IP2.1 概念2.2 数据传输过程 3. 对应关系4. 例子4.1 发送数据包4.2 传输数据包4.3 接收数据包 1. OSI参考模型 1.1 概念 OSI模型&#xff08;Open System Interconnection Reference Model&#xff09;&a…

linux 常用脚本搜集(nginx) —— 筑梦之路

作为搜集之用 nginx acc日志分析 #!/bin/bash # 日志格式: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" LOG_FILE$1 echo "…