Linux之gcc/g++与动静态库初步认识

概念

gcc (GNU Compiler Collection) 和 g++ 是 Linux 系统上最常用的编译器。它们是 GNU 组织开发的一套开源编译器工具集。

gcc:

  • gcc 是 GNU 编译器集合中的 C 语言编译器, 只能编译C语言
  • 它支持多种 C 语言标准(如 ANSI C、ISO C89、ISO C99)以及一些扩展特性.
  • gcc 可以将 C 语言源代码编译成可执行文件,或者生成汇编代码目标文件.

g++:

  • g++ 是 GNU 编译器集合中的 C++ 语言编译器, C/C++都可以编译.
  • 它在 gcc 的基础上添加了对 C++ 语言的支持, 包括标准 C++ 和一些扩展特性.
  • g++ 可以将 C++ 源代码编译成可执行文件, 或者生成汇编代码目标文件.

 gcc 的使用

格式: gcc [选项] 要编译的文件 [选项] [目标文件]

选项:

-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S 编译到汇编语言不进行汇编和链接
-c 编译到目标代码
-o 文件输出到 文件 

-std=99 使用C99标准来编译

直接 g​cc + 源文件 编译的话, 他会直接完成整个翻译过程, 自动生成一个名为a.out的可执行文件:

这个可执行文件的名字是可以自己指定的:

"-o" 选项用于指定生成的可执行文件或目标文件的名称.

gcc 原文件名 -o 新生成文件名 或者 gcc -o 新生成文件名 原文件名
两种写法都可以,只要保证-o之后是新生成文件名即可

 预处理(预编译)

预处理阶段主要完成头文件的展开宏替换条件编译去注释等工作。 

选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。
选项“-o”是指目标文件,“.i”文件为已经过预处理的C原始程序。

预处理之后文件大小大了很多。

然后我们用vim打开,分屏对比一下:

发现预处理阶段完成了头文件的包含,宏替换,条件编译和去注释 

编译 

在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。

“-S”选项只进行编译而不进行汇编,生成汇编代码。

这里我们不指定名字的话,他自动把生成的文件命名为.s后缀的(编译之后文件后缀为.s)
当然我们还可以自己指定 :

这里里面放的其实就是对应的汇编代码 

汇编 

汇编阶段是把编译阶段生成的“.s”文件转成可重定位二进制文件

可以使用指令od查看二进制文件: 

这个我们是看不懂的,它是一种二进制文件 

 链接

链接过程是将多个可重定位目标文件(.o文件)以及库文件组合在一起,生成最终的可执行文件

(.o文件 + 系统库 = 可执行程序)

直接对汇编生成的.o文件进行gcc就可以生成最终的可执行程序:
 


库函数的命名和分类(动静态库) 

为什么我们在Linux上可以进行C/C++代码的编译链接这些动作呢?

其实其中一个比较重要的原因就是Linux提供了这些语言所需要的开发库,如标准C库(libc)、标准C++库(libstdc++)以及其他各种系统库和第三方库。这些库提供了大量的函数和工具,方便开发者编写各种类型的应用程序。

那我们可以看一下我们当前的Linux系统上都提供了那些库:

ls /usr/include

 ldd:ldd命令用于打印一个可执行文件或共享库文件依赖的动态链接库(shared library)列表。它会递归地检查可执行文件或共享库文件所依赖的其他库文件,以及这些依赖的库文件的依赖,一直到所有依赖的库文件列表打印完毕。

 libc.so.6其实就是Linux中的C标准库。

 在这里涉及到一个重要的概念——库(函数库)

我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
最后的答案是:系统把这些函数实现都做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用 

函数库一般分为静态库和动态库两种:

静态库

静态库(Static Library) 类似于你个人的书包,你从图书馆中选择了一些书籍,把它们拷贝到你的书包里。这些书籍是你个人拥有的,可以在需要的时候直接使用。当你需要使用这些书籍时,你只需从书包中取出,不需要依赖图书馆,也不会影响其他学生。

在编程中,静态库是在编译时将库的代码和程序代码链接在一起,形成一个单独的可执行文件。这意味着静态库的代码被复制到了最终的可执行文件中(这种链接方式我们称为静态链接),因此生成的文件比较大, 程序在运行时不需要外部的库文件依赖。这样做的好处是,程序更加独立,可以在不同的系统中运行,不受外部环境的影响。但是生成的文件比较大。
其后缀名一般为“.a”

动态库(Dynamic Library) 类似于图书馆中的共享书架,每个学生都可以访问这些书架上的书籍。当你需要使用这些书籍时,你可以从书架上取出,使用完毕后放回书架上供其他人使用。这意味着多个程序可以共享同一个动态库,减少了存储空间的占用。 

在编程中,动态库是在运行时由操作系统加载的库文件,程序在运行时需要由链接器引入动态库,才能使用其中的函数或资源。可执行文件中只包含对库函数的引用或者说地址,而不复制库的代码和数据(动态链接)。这样做的好处是,多个程序可以共享同一个动态库,减少了内存的占用和可执行文件的大小。

动态库一般后缀名为“.so”,如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。

总结:

动态库是C/C++或其他第三方提供的所有方法的集合,被所有程序以动态链接的方式关联起来,也就是把库中要链接的函数的入口地址拷贝到可执行程序中.

静态库是C/C++或其他第三方提供的所有方法的集合,被所有程序以拷贝的方式,将需要的代码拷贝至自己的可执行程序当中.

注意:

Linux下: .so 是动态库 .a 是静态库

Windows下: .dll 是动态库 .lib 是静态库

 动静态库的优缺点

动态库:

优点:形成的可执行程序体积较小,节省资源
缺点:要找函数地址,会稍慢一点,并且有强依赖性

静态库: 

优点:无视库,可以独立运行
缺点: 体积太大,浪费资源

如何进行静态链接

在我们的Linux上,默认只有动态库,进行的是动态链接

如果想使用静态库编译代码:

使用指令: gcc code.c -static

不过呢,一般我们的Linux上默认只有动态库,所以如果想进行静态链接的话,需要先安装静态库:

yum install -y glibc-static libstdc++-static

这条指令是安装C和C++的静态库

 安装完静态库之后在编译时加上-static对生成的文件采用静态链接:

可以看到静态链接生成的可执行文件比动态链接生成的大了很多。 


关于编译器的自举:

“C语言本身用什么语言写的?”

换个角度来问,其实是:C语言在运行之前,得编译才行,那C语言的编译器从哪里来?用什么语言来写的?如果是用C语言本身来写的,到底是先有蛋还是先有鸡?

我们假设世界上不存在任何编译器, 先从机器语言说起,看看怎么办。

1. 机器语言可以直接被CPU执行,不需要编译器。

2. 然后是汇编语言, 汇编语言虽然只是机器语言的助记符,但是也需要编译成机器语言才能执行,没办法只能用机器语言来写这第一个编译器了(以后就不用了)。

3. 汇编语言的问题解决了,就往前迈进了一大步,这时候就可以用汇编语言去写C语言的编译器,我们说这是C编译器的老祖宗。

4. 有了这个老祖宗,就可以编译任意的C语言程序了,那是不是可以用C语言本身写一个编译器?只要用老祖宗编译一下就可以了。

5. OK, 这么一层层上来,终于得到了一个用C语言写的编译器, 真是够麻烦的。到这个时候,之前那个汇编写的C语言编译器就可以抛弃了。

当然,如果在C语言之前,已经出现了别的高级语言,例如Pascal,那就可以用Pascal来写一个C语言的编译器。第一个Pascal的编译器据说使用Fortran写的。而作为第一个高级语言的Fortran,它的编译器应该是汇编语言写的。

二进制穿孔纸带

2

关于编译器,这里边有个有趣的传说

传说Unix 发明人之一的 Ken Thompson在贝尔实验室,大摇大摆的走到任何一台Unix机器前,输入自己的用户名和密码,就能以root的方式登录!

贝尔实验室人才济济,另外一些大牛发誓要把这个漏洞找出来,他们通读了Unix的C源码,终于找到了登录的后门, 清理后门以后编译Unix , 运行, 可是Thompson 还是能够登录进去。

有人觉得可能是编译器中有问题,在编译Unix的时候植入了后门, 于是他们又用C语言重新写了一个编译器,用新的编译器再次编译了Unix, 这下总算天下太平了吧。

可是仍然不管用, Thompson 依然可以用root登录,真是让人崩溃!

后来Thompson 本人解开了秘密,是第一个C 语言编译器有问题, 这个编译器在编译Unix源码的时候,当然会植入后门, 这还不够,更牛的是,如果你用C 语言写了一个新编译器,肯定也需要编译成二进制代码啊,用什么来编译,只有用Thompson写的那第一个编译器来编译,好了, 你写的这个编译器就会被污染了,你的编译器再去编译Unix , 也会植入后门 :-)

说到这里我就想起了几年前的XcodeGhost 事件,简单来说就是在Xcode(非官方渠道下载的)中植入了木马,这样XCode编译出的ios app都被污染了,这些app就可以被黑客利用做非法之事。

虽然这个XCodeGhost和Thompson的后面相比差得远,但是提醒我们,下载软件的时候要走正规渠道,从官方网站下载,认准网站的HTTPS标准,甚至可以验证一下checksum。

3

可能有人问:我用汇编写一段Hello World都很麻烦,居然有人可以用它写复杂的编译器?这可能吗?

当然可能,在开发第一代Unix的时候,连C语言都没有, Ken Thompson 和 Dennis Ritchie 可是用汇编一行行把Unix敲出来的。 WPS第一版是求伯君用汇编写出来的, Turbo Pascal 的编译器也是Anders 用汇编写出来的,大神们的能力不是普通人能想象得到的。

对于编译器来说,还可以采用“滚雪球”的方式来开发: 

还是以C语言为例,

1. 第一个版本可以先选择C语言的一个子集,例如只支持基本的数据类型,流程控制语句,函数调用...... 我们把这个子集称为C0。然后用汇编语言写个编译器,只搞定这个语言的子集C0,这样写起来就容易不少。

2. C0这个语言可以工作了,然后我们扩展这个子集,例如添加struct,指针...... ,把新的语言称为C1。那C1这个语言的编译器由谁来写? 自然是C0。

3. 等到C1可以工作了,再次扩展语言特性,用C1写编译器,得到C2。

4. 然后是C3, C4...... 最后得到完整的C语言。

这个过程被称为bootstraping , 中文叫做自举。

作者:关于编程哪些事
链接:https://zhuanlan.zhihu.com/p/136183943
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

了解了编译器的自举之后,发现C语言的翻译过程正是它的历史过程的逆过程: 

先从C语言预处理编译为汇编,再从汇编翻译(链接)为二进制, 因为大佬总是懂得站在巨人的肩上, 有前人编写了从汇编到二进制的代码,  那就不需要大费力气重写从C语言到二进制的代码了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值