编译器的详细讲解
执行程序的组装过程
用gcc生成静态库和动态库
我们通常把一些公用函数制作成函数库,供其它程序使用。函数库分为静态库和动态库两种。静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。
(1) 第 1 步:编辑生成例子程序 hello.h、hello.c 和 main.c。
先创建一个作业目录,保存本次练习的文件。
mkdir test1
cd test1
然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的 3 个文件。
hello.c是函数库的源程序,其中包含公用函数 hello,该函数将在屏幕上输出"HelloXXX!"。
#include <stdio.h>
void hello(const char *name)
{
printf("Hello %s!\n", name);
}
hello.h为该函数库的头文件。
代码如下:
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif //HELLO_H
main.c为测试库文件的主程序。
#include "hello.h"
int main()
{
hello("everyone");
return 0;
}
在主程序中调用了公用函数 hello
(2)第 2 步:将 hello.c 编译成.o 文件。
无论静态库,还是动态库,都是由.o 文件创建的。因此,我们必须将源程序 hello.c 通过 gcc 先编译成.o 文件。在系统提示符下键入以下命令得到 hello.o 文件。
gcc -c hello.c
我们运行 ls 命令看看是否生存了 hello.o 文件。
# ls
在 ls 命令结果中,我们看到了 hello.o 文件。
(3)第 3 步:由.o 文件创建静态库。
静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为.a。创建静态库用 ar 命令。在系统提示符下键入以下命令将创建静态库文件libmyhello.a。
ar -crv libmyhello.a hello.o
运行 ls 命令查看结果:
ls
成功生成静态库
Linux ar命令用于建立或修改备存文件,或是从备存文件中抽取文件。ar可让您集合许多文件,成为单一的备存文件。在备存文件中,所有成员文件皆保有原来的属性与权限。
ar[-dmpqrtx][cfosSuvV][a<成员文件>][b<成员文件>][i<成员文件>][备存文件][成员文件]
(4) 第 4 步:在程序中使用静态库。
在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明静态库名,gcc 将会从静态库中将公用函数连接到目标文件中。
在main.c中,我们包含了静态库的头文件hello.h,然后在主程序 main 中直接调用公用函数 hello。下面先生成目标程序 hello,然后运行 hello 程序看看结果如何。
gcc main.c libmyhello.a -o hello
gcc -o: 生成执行文件
运行hello
./hello
我们删除静态库文件试试公用函数 hello 是否真的连接到目标文件 hello 中了。
rm libmyhello.a
再次运行程序
程序照常运行,静态库中的公用函数已经连接到目标文件中了。
(5) 第 5 步:由.o 文件创建动态库文件。
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其文件扩展名为.so。
在系统提示符下键入以下命令得到动态库文件 libmyhello.so。
gcc -shared -fPIC -o libmyhello.so hello.o
(6) 第 6 步:在程序中使用动态库;
在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。
gcc -o hello main.c -L. -lmyhello
运行hello
程序在运行时,会在/usr/lib 和/lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。我们需要将文件 libmyhello.so 复制到目录/usr/lib 中
mv libmyhello.so /usr/lib
如果无法移动,可以在mv前面加sudo,用管理员身份运行
sudo mv libmyhello.so /usr/lib
Linux sudo命令以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。
运行hello
成功了。这也进一步说明了动态库在程序运行时是需要的。
静态库.a与.so库文件的生成与使用
先创建一个作业目录,保存本次练习的文件。
mkdir test2
cd test2
然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的四个文件 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
1.4、运行test
./test
2、共享库.so 文件的生成与使用
2.1、生成目标文件(xxx.o() 此处生成.o 文件必须添加"-fpic"(小模式,代码少),否则在生成.so文件时会出错)
gcc -c -fpic A1.c A2.c
2.2、生成共享库.so 文件
gcc -shared *.o -o libsofile.so
2.3、使用.so 库文件,创建可执行程序
gcc -o test test.c libsofile.so
2.4、运行
./test
这是因为动态库只在/lib and/usr/lib下搜索对应的.so文件。
用以下代码移动文件
sudo cp libsofile.so /usr/lib
运行成功
主程序文件main.c调用两个函数x2x和x2y进行运算
main.c主程序:
#include<stdio.h>
#include<x2.h>
int main()
{
int a=25;
int b;
float c;
b=x2x(a);
c=x2y(a,b);
printf("%f",c);
return 0;
}
x2x.c用于实现25到100的累加
x2x.c主程序:
#include<stdio.h>
int x2x(int a)
{
for(int i=26;i<=100;i++)
a=a+i;
return a;
}
x2y用于除法
x2y.c代码:
include<stdio.h>
float x2y(int a,int b)
{
float c;
c=b/a;
return c;
}
x2.h代码:
#ifndef X2_H
#define X2_H
int x2x(int a);
float x2y(int a,int b);
#endif //X2_H
用gcc -c生成main.o,x2x.o,x2y.o文件
方法一、静态库
生成静态文件库libx2x.a和libx2y.a
以lib为前缀,ar为命令符
在程序中使用静态库,生成可执行文件x2
gcc main.c libx2x.a libx2y.a -o x2
运行x2
方法二、动态库
创建动态库
gcc -shared -fPIC -o libx2x.so x2x.o
gcc -shared -fPIC -o libx2y.so x2y.o
文件 libx2x.so,libx2y.so 复制到目录/usr/lib 中
sudo mv libx2x.so /usr/lib
sudo mv libx2y.so /usr/lib
生成执行文件
gcc main.c libx2x.so libx2y.so -o x2
运行
静态库与动态库的比较
静态库
动态库
对比两个的文件大小,动态库的占用量是大于静态库的
gcc编译工具集中各软件的用途
Linux GCC 常用命令
GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在
示例程序:
创建test.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
一步到位的编译指令是:
gcc test.c -o test
预处理(可以让编译器在预处理后停止,并输出预处理结果)
gcc -E test.c -o test.i 或 gcc -E test.c
编译为汇编代码(Compilation)
预处理之后,可直接对生成的 test.i 文件编译,生成汇编代码:
gcc -S test.i -o test.s
汇编(Assembly)
对汇编代码文件 test.s,gas 汇编器负责将其编译为目标文件,如下:
gcc -c test.s -o test.o
图中test为一步生成程序生成的,不必理会
连接(Linking)
gcc 连接器是 gas 提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。对于上一小节中生成的 test.o,将其与C标准输入输出库进行连接,最终生成程序 test
gcc test.o -o test
多个程序文件的编译
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc test1.o test2.o -o test
检错 -pedantic 选项能够帮助程序员发现一些不符合ANSI/ISO C 标准的代码,但不是全部
gcc -pedantic illcode.c -o illcode
库文件连接
编译成可执行文件
gcc –c –I /usr/dev/mysql/include test.c –o test.o
链接
gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
强制链接时使用静态链接库
gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
GCC 编译器背后的故事
GCC:
GCC(GNU C Compiler)是编译工具。
Binutils:
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、 size 等。这 一组工具 是开发和 调试不可 缺少的工具 ,分别简 介
如下:
(1) addr2line:用 来将程序 地址转 换成其所 对应的程 序源文 件及所对 应的代 码
行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对
应的源代码位置。
(2) as:主要用于汇编,有关汇编的详细介绍请参见后文。
(3) ld:主要用于链接,有关链接的详细介绍请参见后文。
(4) ar:主要用于创建静态库。为了便于初学者理解,在此介绍动态库与静态库的概念:
1、如果 要将 多个 .o 目标 文件 生成 一个 库文 件, 则存 在两 种类 型的 库, 一种是静态库,另一种是动态库。
2、在 windows 中 静态 库是 以 .lib 为 后缀 的文 件 ,共 享库 是以 .dll 为 后缀的 文 件 。 在 linux 中 静 态 库 是 以 .a 为 后 缀 的 文 件 ,共 享 库 是 以 .so 为 后 缀的文件。
3、静 态 库 和 动 态 库 的 不 同 点 在 于 代 码 被 载 入 的 时 刻 不 同 。 静 态 库 的 代 码 在 编译 过 程 中 已 经 被 载 入 可 执 行 程 序 , 因 此 体 积 较 大 。共 享 库 的 代 码 是 在 可 执行 程 序 运 行 时 才 载 入 内 存 的 , 在 编 译 过 程 中 仅 简 单 的 引 用 , 因 此 代 码 体 积较 小 。 在 Linux 系 统 中 , 可 以 用 ldd命 令 查 看 一 个 可 执 行 程 序 依 赖 的 共 享库。
4、如 果 一 个 系 统 中 存 在 多 个 需 要 同 时 运 行的 程 序 且 这 些 程 序 之 间 存 在 共 享库,那么采用动态库的形式将更节省内存。
(5) ldd:可以用于查看一个可执行程序依赖的共享库。
(6) objcopy:将一种对象文件翻译成另一种格式,譬如将.bin 转换成.elf、或
者将.elf 转换成.bin 等。
(7) objdump:主要的作用是反汇编。有关反汇编的详细介绍,请参见后文。
(8) readelf:显示有关 ELF 文件的信息,请参见后文了解更多信息。
(9) size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等
C 运行库:
C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。
C 语言标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此,C 语言编译器通常需要一个 C 运行时库(C Run Time Libray,CRT)的支持。C 运行时库又常简称为 C 运行库。与 C 语言类似,C++也定义了自己的标准,同时提供相关支持库,称为 C++运行时库。
准备工作:
先创建一 个工作目录 test0,然后用文本编辑器生成一个 C 语言编写的简单 hello.c 程序为示例,其源代码如下所示:
#include <stdio.h>
int main(void)
{
printf("Hello World! \n");
return 0;
}
预处理
gcc -E hello.c -o hello.i
hello.i代码片段
编译
gcc -S hello.i -o hello.s
hello.s代码片段
汇编
gcc -c hello.s -o hello.o
链接
可翻看前面示例,在此不再详细介绍
ar -crv libmyhello.a hello.o //静态
gcc -shared -fPIC -o libmyhello.so hello.o //动态
size hello //使用 size 查看大小
$ ldd hello //可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc动态库
分析 ELF 文件
ELF 文件的段
readelf -S hello
反汇编 ELF
安装并使用nasm
使用 sudo apt-get install nasm安装nasm 并用nasm -version查看是否安装成功
把示例文件hello.asm复制到linux中,或者直接放到共享文件夹下:
对示例代码“hello.asm”编译生成可执行程序:
查看大小:
对比c代码:
显然,汇编代码占用内存小得多。
借助第三方库函数完成代码设计
1)了解Linux 系统中终端程序最常用的光标库(curses)的主要函数功能,写出几个基本函数名称及功能;
2)在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client” 和"适用于Linux的Windows子系统"(后面会使用)。 然后打开一个cmd命令行窗口,命令行输入 telnet bbs.newsmth.net,以游客身份体验一下即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)。
3)在Ubuntu中用 sudo apt-get install libncurses5-dev 安装curses库
ubunt下,该库的头文件在 /usr/include ,库在/usr/lib/i386-linux-gnu/libc.so。
4)体验Linux 环境下C语言编译贪吃蛇游戏(http://www.linuxidc.com/Linux/2011-08/41375.htm)复制代码到ubuntu下,保存为snake.c
然后在该处打开终端输入输入
cc snake.c -lcurses -o mysnake //生成可执行文件
然后输入./mysnake 及可运行贪吃蛇小游戏,如下图。