1 GCC编译四步骤

GCC编译可执行程序四步骤:预处理 编译 汇编 链接

hello.c>>>>hello.i>>>>>hello.s>>>>>>hello.o

预处理:展开宏/头文件 替换条件编译 删除注释/空行/空白 gcc -E -o hello.i

编译:检查语法规范 汇编语言 gcc -S -o hello.s

汇编:将汇编指令翻译成机器指令 gcc -c hello.s -o hello.o

链接:数据段合并,数据地址回填。将汇编文件变成可执行文件 gcc hello.o -o a.out

四个阶段中,编译阶段消耗时间和系统资源最多;

2 GCC编译选项

在文件夹中含有 hello.h 和 hello.c 头文件,当两者在同一个文件夹中,可以一起编译

gcc hello.c -o main

而在实际工程中,一般会放置多个文件夹,比如源码放在在src中,头文件放入inc文件夹中,编译的时候,就需要指定头文件目录,使用-I

.
├── 123 xyz
├── 1.c
├── 1.tar.gz
├── a.out
├── exe
├── exe1
├── hello.c
├── hello.i
├── hello.s
├── inc
│ └── hello.h
└── test.zip

hello.c的头文件在hello.h中,编译命令是:

gcc hello.c -Iinc -o main

-I和后面参数之间的空格可以灵活调整;

-c 只编译生成.o文件,不进行链接;(只做预处理,编译和汇编),得到的是二进制文件;(可以使用linux 下的file命令查看文件的的类型)

-g 包含调试信息,编译时添加调试文件(加入-g选项,可以进行调试,不加入无法调试);生成的调试文件可以用于gdb调试;

-On n=0~3,编译优化,n越大优化得月多(debug阶段不建议添加优化选项)

-Wall 提示更多警告的信息

-D 编译时定义宏,注意-D和之间没有空格;

具体见下面的代码,代码如果没有宏定义的HELLO,会缺少对HI的定义;而可以通过gcc -D选项,编译动态增加宏定义。用这种方法,可以实现开关功能,比如:

#ifdef SWITCH

printf(“xxxxx”);

#endif

在编译选项中,可以通过是否添加动态宏定义选项,决定是否显示调试信息。

#include <stdio.h>
#ifdef HELLO
#define HI 20
#endif
int main(int argc, char** argv)
{
​    printf("hi=%d\n",HI);
​    return 0;
}
20daniel@daniel-Vostro-5471:~/文档/OS/test$ gcc hello_num.c -o a.out
hello_num.c: In function ‘main’:
hello_num.c:7:22: error: ‘HI’ undeclared (first use in this function)
    7 |     printf("hi=%d\n",HI);
      |                      ^~
hello_num.c:7:22: note: each undeclared identifier is reported only once for each function it appears in
daniel@daniel-Vostro-5471:~/文档/OS/test$ gcc hello_num.c -o a.out -D HELLO
daniel@daniel-Vostro-5471:~/文档/OS/test$ ./a.out
hi=20
daniel@daniel-Vostro-5471:~/文档/OS/test$ 

-E 生成预处理文件

-M 生成.c文件与头文件依赖关系,以用于Makefile,包括系统库的头文件

-MM 生成.c文件与头文件依赖关系,以用于Makefile,不包括系统库的头文件

-l 用于指定动态库名(动态库和静态库均可使用,比如linux下常用的多线程库,需要添加 -l pthread)

-L 包含指定动态库的路径(动态库和静态库均可使用)

-fPIC 用于将代码生成动态库所需要的ELF文件 gcc -c add.c -o add.o -fPIC

3 静态库和共享库

程序库可以分为静态库(static library)和共享库(shared object,动态库)

静态库,在可执行程序运行之前,就将静态库加入到执行码中,成为程序的一部分;

动态库,是在执行程序启动时动态加载到程序中,可以被多个执行程序共享使用(即多个应用程序可以共享使用该库)。

当多个应用程序都需要使用某个库时,动态库会比静态库节省更多的空间;

静态库:重要用于对程序的空间要求较低,而时间要求较高的核心程序中;

动态库:对时间要求较低,对空间要求更高;

建议库开发人员使用共享库,优点是库独立,便于维护和更新;而静态库更新比较麻烦,一般不推荐。

本节所使用的执行程序和库都是ELF(Executable and Linking Format)格式。

静态库可以认为是一些目标代码的集合,按照习惯,一般以“.a”作为文件后缀,可以使用ar(archiver)命令创建静态库

3.1 静态库生成及使用

ar rcs libmylib.a file1.o rcs是静态库参数,记住。库最好以lib开头,以“.a”结尾。

//add.c
int add(int a, int b)
{
​    return a+b;
}
//sub.c
int sub(int a, int b)
{
​    return a-b;
}

例如:

.
├── add.c
├── add.o
├── libcal.a
├── sub.c
└── sub.o

daniel@daniel-Vostro-5471:~/文档/OS/test/static$ gcc -c add.c -o add.o	#生成.o文件,只编译不链接
daniel@daniel-Vostro-5471:~/文档/OS/test/static$ gcc -c sub.c -o sub.o	#生成.o文件
daniel@daniel-Vostro-5471:~/文档/OS/test/static$ ar rcs libcal.a add.o sub.o	#将两个.o文件打包成为一个libcal.a的静态库
daniel@daniel-Vostro-5471:~/文档/OS/test/static$ file libcal.a	#调用查看libcal.a的类型
libcal.a: current ar archive

编译出错:会有行号信息,提示出错;

链接出错:不会再有行号信息,提示出错;

collect2:链接器

collect2:error: ld returned 1 exit status (提示了链接出错)

使用静态库的方法,是将主程序和静态库一起编译,例如:

gcc test.c libcal.a -o test #test.c是主程序,libcal.a是上一步生成的静态库;

要把主函数写在库文件之前

也可以先把test.c文件编译成为test.o文件

gcc -c test.c -o test.o #添加-Wall信息,会提示没有声明 add和sub函数

再把test.o和libcal.a相互链接

gcc test.o libcal.a -o main

//test.c文件
//不需要包含add.c和sub.c文件,也可以加进行编译工作,但是会提示错误
#include <stdio.h>
int main(int argc, char** argv)

{int a=20,b=10;printf("a+b=%d\n",add(a,b));printf("a-b=%d\n",sub(a,b));return 0;
}

在主函数调用其它函数时,首先检查函数定义,如果没有函数定义,会寻找函数声明;如果没有函数声明,编译器会给函数生成隐式定义;

对于上例子,如果没有声明add函数和sub函数,编译器会根据函数名和调用的参数,自动声明为:

int add(int, int);
int sub(int, int);

而对于本例来说,正好正确;在如果隐式声明推测的函数声明和实际声明不同,会导致函数调用失败!!!

正确做法是,给静态库整体做一个头文件,这个头文件有静态库所有函数的声明,然后在主函数中包含这个头文件;头文件内容如下:

#ifndef _CAL_H
#define _CAL_H
int add(int, int);
int sub(int, int);
#endif

标准的目录结构,使用inc存放头文件,使用lib存放静态库,使用src存放源文件,编译命令如下:

daniel@daniel-Vostro-5471:~/文档/OS/test/static$ tree
├── inc
│   └── cal.h
├── lib
│   └── libcal.a
├── main
├── src
│   ├── add.c
│   ├── sub.c
│   └── test.c
└── test.c
3 directories, 7 files
daniel@daniel-Vostro-5471:~/文档/OS/test/static$ gcc test.c ./lib/libcal.a -I./inc -o main

3.2 动态库生成及使用

没有调用到动态库内部的函数的时候,动态库不会加载使用;只有调用到动态库的内部的函数的时候,动态库才会被加载到内存;

1.将 .c 文件生成 .o文件;

动态库要求生成与位置无关的代码(函数调用之前需要将其地址固定)

数据段合并和地址回填 延迟绑定(动态库函数的地址比主函数的其它调用函数分配地址要晚)

结论:制作动态库的.o文件和静态库有区别,生成位置无关文件,借助 -fPIC选项

gcc -c add.c -o add.o -fPIC

2.使用gcc 和 -shared选项制作静态库

gcc -shared -o lib库名.so add.o sub.o

3.编译可执行程序时,指定所使用的动态库, -l和-L

-l:用来指定库名 -L:用来指定库路径

gcc test.c -o a.out -lcal(cal是库名) -L./lib

4.运行可执行程序;

./a.out

示例:文件和之前相同,目录结构如下:

aniel@daniel-Vostro-5471:~/文档/OS/test/dynamic$ tree
├── a.out
├── inc
│   └── cal.h
├── lib
│   ├── libcal.a
│   └── libcal.so
├── main
├── out
├── src
│   ├── add.c
│   ├── add.o
│   ├── sub.c
│   ├── sub.o
│   └── test.c
└── test.c
3 directories, 12 files
gcc -c add.c -o add.o -fPIC
gcc -c sub.c -o sub.o -fPIC
gcc -shared -o lib静态库名.so  ../src/add.o ../src/sub.o
#使用下列语句编译,但是编译之后运行程序,终端报错
gcc test.c -l静态库名 -L./lib -Iinc -o main
#上面语句中的静态库名,不包含lib前缀和.so的扩展名,比如
gcc test.c -lcalc -L./lib -I./inc -o main

报错原因是找不到动态文件。

可以通过下面的命令查看运行程序运行之后,需要加载那些动态库,以及动态库的路径

ldd a.out

原因:

链接器:工作于链接阶段,工作时需要 -l和-L选项(用于编译静态库)

动态链接器:工作于程序运行阶段,工作时需要提供动态库所在目录位置;

解决方法:

**1.环境变量是依赖于终端的,切换终端之后,新的终端的环境变量会失效

#环境变量:
LD_LIBRARY_PATH=./lib
#环境变量生效:
export LD_LIBRARY_PATH=./lib
#上面填入的是相对路径,如果要一直生效,最好填入绝对路径
#运行程序
./a.out

**2.一劳永逸的做法是修改bash的配置文件:

(1) 通过gedit或者vi修改bash的配置文件;

#脚本的名字为.bashrc

gedit ~/.bashrc
#或者使用vim打开
vi ~/.bashrc

(2)在终端中添加动态库路径

export LD_LIBRARY_PATH=动态库路径

建议使用绝对路径

(3)使脚本文件生效,通过运行脚本文件实现、

. .bashrc	
#或者
source .bashrc
#或者重启终端,每次重启终端,bashrc都会运行

**3.永久解决方法3

拷贝自定义动态库到标准C库(不推荐)

标准C库也是通过动态库进行加载的,而且可以加载成功,这给我们的提示是可以把这个生成的动态库拷贝到C库的文件夹中

sudo cp libmymath.so /lib

然后把原来设置的环境变量删掉。

**4.配置文件法

#打开配置文件

sudo vi /etc/ld.so.conf

在配置文件中写入库文件目录所在位置,最好写入决对目录

让配置文件生效

#-v选项会在终端显示动态库的加载位置

sudo ldconfig -v

运行程序生效

3.3数据段合并和地址回填

32位操作系统的内存使用和分布

4G内核区 kernel
3Gstack 栈区 从上往下用
0~3G 用户区 userheap 堆区 堆区从下往上用
.bss 初始化为非0的数据区
.data 未初始化数据区
.rodata 只读数据区
0.text 代码区

栈区从上往下使用,堆区从下往上使用,堆和栈中间的区域随着使用越来越小;

环境变量和main函数的参数,存放在栈底,即靠近3G和4G边缘的位置;

.so 加载位置位于栈区和堆区中间

.text 代码段,是常量,不能被随意改变,属性 ro(只读)

.rodata 只读数据段,存放常量和只读变量,可以和代码段合并

代码段和只读数据段的合并可以节省内存的使用空间,原来.text和.rodata都需要单独以4kB为单位占用内存,合并之后,可以一起以4kB单位占用内存;

4k 内存页的大小,是内存的分割的最小单位

使用磁盘,每个扇区最小512kB

.data和.bss 都是rw,可读可写,也可以进行数据段合并;

3.4 注意事项

1.动态库是否加载到内存,取决于程序运行与否;

2.动态库每次加载的位置不固定;

3.动/静态库共存时,系统默认使用动态库;

Logo

更多推荐