编译器背后的故事
一、用 gcc 生成 .a 静态库和 .so 动态库
1.第一步:编辑生成例子程序 hello.h、hello.c 和 main.c。
先创建一个作业目录,保存本次练习的文件。
#mkdir test1
#cd test1
然后用vi文本编辑器生成三个文件hello.h、hello.c、main.c。
程序1:hello.h
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif //HELLO_H
程序2:hello.c
#include <stdio.h>
void hello(const char *name){
printf("Hello %s!\n", name);
}
程序3:main.c
#include "hello.h"
int main() {
hello("everyone");
return 0; }
2.第 2 步:将 hello.c 编译成.o 文件。
无论静态库,还是动态库,都是由.o 文件创建的。因此,我们必须将源程序 hello.c 通过 gcc 先编译成.o 文件。
gcc -c hello.c
运行 ls 命令看看是否生存了 hello.o 文件,结果如下:
3.第 3 步:由.o 文件创建静态库。
静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为.a。例如:我们将 创建的静态库名为 myhello,则静态库文件名就是 libmyhello.a。在创建和使用静态库时, 需要注意这点。创建静态库用 ar 命令。在系统提示符下键入以下命令将创建静态库文件 libmyhello.a。
ar -crv libmyhello.a hello.o
运行ls命令查看结果:
4.第 4 步:在程序中使用静态库。
静态库制作完了,如何使用它内部的函数呢?只需要在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明静态库名,gcc 将会从 静态库中将公用函数连接到目标文件中。注意,gcc 会在静态库名前加上前缀 lib,然后追 加扩展名.a 得到的静态库文件名来查找静态库文件。 在程序 3:main.c 中,我们包含了静态库的头文件 hello.h,然后在主程序 main 中直接调用公用函数 hello。下面先生成目标程序 hello,然后运行 hello 程序看看结果如何
gcc main.c libmyhello.a -o hello
5.第 5 步:由.o 文件创建动态库文件。
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其 文件扩展名为.so。例如:我们将创建的动态库名为 myhello,则动态库文件名就是 libmyh ello.so。用 gcc 来创建动态库。 在系统提示符下键入以下命令得到动态库文件 libmyhello.so。
gcc -shared -fPIC -o libmyhello.so hello.o
会发现出现错误:
经过查询由于我的系统是Ubuntu16,所以需要在hello.c生成hello.o文件的时候添加 -fPIC选项
即:gcc -fpic -c hello.c
这样就可以正常创建动态库文件了
6.第 6 步:在程序中使用动态库
在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。我们先运行 gcc 命令生成目标文件,再运行它看看结果。
gcc main.c libmyhello.so -o hello
不会出错(没有 libmyhello.so 的话,会出错),但是接下来./hello 会提示出错,因为虽然连接时用的是当前目录的动态库,但是运行时,是到 /usr/lib 中找库文件的,将文件 libmyhello.so 复制到目录/usr/lib 中就 OK 了)
结果如下:
经过观察发现使用静态库和使用动态库编译成目标程序使用的 gcc 命令完全一样。
二、Linux下静态库.a与.so库文件的生成与使用
先创建一个作业目录:
#mkdir test2
#cd test2
然后用vi文本编辑器生成四个文件A1.c、A2.c、A.h、test.c。
A1.c
#include <stdio.h>
void print1(int arg){
printf("A1 print arg:%d\n",arg);
}
A2.c
#include <stdio.h>
void print2(char *arg){
printf("A2 printf arg:%s\n", arg);
}
A.h
#ifndef A_H
#define A_H
void print1(int);
void print2(char *);
#endif
test.c
#include <stdlib.h>
#include "A.h"
int main(){
print1(1);
print2("test");
exit(0);
}
结果如下:
1、静态库.a 文件的生成与使用
1.1、生成目标文件(xxx.o)
gcc -c A1.c A2.c
1.2、生成静态库.a文件
ar crv libafile.a A1.o A2.o
1.3、使用.a 库文件,创建可执行程序(若采用此种方式,需保证生成的.a 文件与.c 文件保 存在同一目录下,即都在当前目录下)
gcc -o test test.c libafile.a
./test
2、共享库.so 文件的生成与使用
2.1、生成目标文件(xxx.o() 此处生成.o 文件必须添加"-fpic"(小模式,代码少),否则在生成.so 文件时会出错)
gcc -c -fpic A1.c A2.c
2.2、生成共享库.so 文件
gcc -o test test.c libsofile.so
./test
会发现以下错误
接下来运行ldd test,查看链接情况
发现确实是找不到对应的.so 文件。 这是由于 linux 自身系统设定的相应的设置的原因,即其只在/lib and /usr/lib 下搜索对应 的.so 文件,故需将对应 so 文件拷贝到对应路径。然后再次运行./test
sudo cp libsofile.so /usr/lib
./test
三、用gcc将静态库与动态库链接生成可执行程序并记录文件大小
1、编写一个算术运算函数float x2x(int a,int b),此函数功能为对两个输入整型参数相加,将结果做浮点数返回,x2y函数功能为两个输入整型参数相减,结果做浮点数返回。main函数代码将调用x2x和x2y,将这3个函数分别写成单独的3个.c文件 。
编写sub1.c:
编写sub2.c:
编写main.c:
编写main.h:
2、用gcc分别编译为3个.o目标文件
gcc -c sub1.c sub2.c
3、将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件
4、 用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小
记录文件的大小
5、将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
生成.so动态库文件
gcc -shared -fPIC -o libsub.so sub1.o sub2.o
用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序
上述错误前文均有解决方案,不再赘述。
记录文件大小:
通过比较发现静态库要比动态库要小很多,生成的可执行文件大小也存在较小的差别
四、Linux GCC常用命令
1、简介 GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C 语言;它现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal 语言、COBOL 语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C 语 言编译器的意思了,而是变成了GNU Compiler Collection 也即是 GNU 编译器家族的意思了。另 一方面,说到 GCC对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。
2、GCC简单编译:
2.1 先创建一个目录test4保存test.c,代码如下:
#include <stdio.h>
int main(void) {
printf("Hello World!\n");
return 0; }
一步到位的编译指令是:gcc test.c -o test
实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译 (Compilation)、汇编 (Assembly)和连接(Linking)。
2.2 gcc编译的四个步骤
预处理:gcc -E test.c -o test.i或gcc -E test.c
编译为汇编代码: gcc -S test.i -o test.s
汇编: gcc -c test.s -o test.o
链接生成可执行文件: gcc test.o -o test
3 、多个程序文件的编译 通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理 这些编译单元。假设有一个由 test1.c 和 test2.c 两个源文件组成的程序,为了对它们进行编译,并 最终生成可执行程序test,可以使用下面这条命令:
gcc test1.c test2.c -o test
如果同时处理的文件不止一个,GCC仍然会按照预处理、编译和链接的过程依次进行。如果深究起来,上面这条命令大致相当于依次执行如下三条命令:c gcc -c test1.c -o test1.o gcc -c test2.c -o test2.o gcc test1.o test2.o -o test
4、检错
bash gcc -pedantic illcode.c -o illcode
-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。或者换句话说,-pedantic 选项能够帮助程序员发现一些不符合 ANSI/ISO C标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的那些情况,才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W 开头,其中最有价值的当数-Wall了,使用它能够使 GCC 产生尽可能多的警告信息。
bash gcc -Wall illcode.c -o illcode
GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优 秀的 Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror 选项,那 么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:
bash gcc -Werror test.c -o test
5 、库文件连接 开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支 持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或 lib、dll)的集合。虽然 Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下;Windows所使用的库文件主要放在 Visual Stido 的目录下的 include 和 lib,以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以 GCC 在编译时必须用自己 的办法来查找所需要的头文件和库文件。 例如我们的程序 test.c 是在 linux 上使用 c 连接 mysql,这个时候我们需要去 mysql 官网下载 MySQLConnectors 的 C 库,下载下来解压之后,有一个 include 文件夹,里面包含 mysql connectors 的头
文件,还有一个 lib 文件夹,里面包含二进制 so 文件 libmysqlclient.so 其中 inclulde
文件夹的路径是/usr/dev/mysql/include,lib 文件夹是/usr/dev/mysql/lib
5.1 编译成可执行文件 首先我们要进行编译 test.c 为目标文件,这个时候需要执行
gcc –c –I /usr/dev/mysql/include test.c –o test.o
5.2 链接 最后我们把所有目标文件链接成可执行文件:
bash gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
Linux 下的库文件分为两大类分别是动态链接库(通常以.so 结尾)和静态链接库(通常以.a 结尾),
二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的。5.3 强制链接时使用静态链接库
默认情况下, GCC 在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。
在/usr/dev/mysql/lib目录下有链接时所需要的库文件 libmysqlclient.so 和 libmysqlclient.a,为了让 GCC在链接时只用到静态链接库,可以使用下面的命令:
gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
静态库链接时搜索路径顺序:
- ld 会去找 GCC 命令中的参数-L
- 再找 gcc 的环境变量 LIBRARY_PATH
- 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的
动态链接时、执行时搜索路径顺序:
- 编译目标代码时指定的动态库搜索路径
- 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
- 配置文件/etc/ld.so.conf 中指定的动态库搜索路径
- 默认的动态库搜索路径/lib
- 默认的动态库搜索路径/usr/lib
有关环境变量:
LIBRARY_PATH 环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量:指定程序动态链接库文件搜索路径
五、as汇编编译器
1.简介
as汇编编译器针对的是AT&T汇编代码风格,Intel风格的汇编代码则可以用nasm汇编编译器编译生成执行程序。
2.在ubuntu中下载安装nasm,对示例代码“hello.asm”编译生成可执行程序
直接执行命令即可安装nasm
sudo apt install nasm
输入hello.asm:
; hello.asm
section .data ; 数据段声明
msg db "Hello, world!", 0xA ; 要输出的字符串
len equ $ - msg ; 字串长度
section .text ; 代码段声明
global _start ; 指定入口函数
_start: ; 在屏幕上显示一个字符串
mov edx, len ; 参数三:字符串长度
mov ecx, msg ; 参数二:要显示的字符串
mov ebx, 1 ; 参数一:文件描述符(stdout)
mov eax, 4 ; 系统调用号(sys_write)
int 0x80 ; 调用内核功能
; 退出程序
mov ebx, 0 ; 参数一:退出代码
mov eax, 1 ; 系统调用号(sys_exit)
int 0x80 ; 调用内核功能
1、使用nasm -f elf64 hello.asm,会生成一个hello.o文件
2、使用ld -s -o hello hello.o ,生成一个可执行文件hello
3、执行./hello输出
3、与“hello world”C代码的编译生成的程序大小进行对比。
nasm生成的可执行程序大小:
C语言生成的程序大小:
对比可知nasm方式编译得到的可执行程序要比C语言编译得到的可执行程序小得多。
六、Linux中的第三方库函数
1、Linux 系统中终端程序最常用的光标库(curses)
1.1 光标库(curses)的主要函数功能
curses是一个在Linux/Unix下广泛应用的图形函数库,作用是可以在终端内绘制简单的图形用户界面。
1.2一些基本函数名称及功能
函数 | 功能 |
---|---|
initscr() | 初始化curses库和ttty |
endwin() | 关闭curses并重置tty |
move(y,x) | 将光标移动至x,y的位置 |
getyx(win,y,x) | 得到目标游标的位置 |
clear()and erase() | 将整个屏幕清楚 |
echochar(ch) | 显示某个字元 |
2、体验即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)
在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client”和"适用于Linux的Windows子系统"(后面会使用)。
然后打开一个cmd命令行窗口,命令行输入 telnet bbs.newsmth.net。
3、在Ubuntu中用 sudo apt-get install libncurses5-dev 安装curses库
curses函数库的头文件和库文件就被分别安装在/usr/include/和/usr/lib/下
七、用gcc编译生成一个终端游戏
将Linux环境下C语言编译实现贪吃蛇游戏内的代码复制到tcs.c中
使用命令进行编译生成可执行文件
gcc tcs.c -lcurses -o tcs
执行文件如图: