1. 编译连接与执行(简易计算器的编译)

128 阅读7分钟

在正式的学习 Makefile 知识之前,本次实验先介绍一些简单的前导知识。实验详细介绍了 GNU GCC 编译和链接的基本方法,通过编译、链接、静态链接、动态链接的实验内容让用户学习和理解 GCC 的基本使用方法。同时,用户也将在实验过程中体会到手动编译链接的低效,从而体会到自动编译的在项目工程管理中的重要性。

知识点

  • GCC 编译的使用方式
  • GCC 链接的使用方式
  • GCC 静态链接的使用方式
  • GCC 动态链接的使用方式
  • GCC 静态链接 + 动态链接混用的方式

代码获取:

通过在 Terminal 中输入以下命令可以将本课程所涉及到的所有源代码下载到在线环境中,作为参照对比进行学习。

wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip && unzip make_example-master.zip && rm make_example-master.zip

命令执行后 WebIDE 的工作区中将会出现一个名为 make_example-master 的文件夹,文件夹中包含了课程所涉及到的源代码,目录结构如图所示:

图片.png

本章项目内容

本章节的源代码位于 /home/project/make_example-master/chapter0 目录中,请在 Terminal 中通过 cd 命令切换至该目录后再进行实验学习。

项目涉及到的代码文件:

  1. main.c: 主要文件
  2. add_minus.c add_minus.h: 加减法 API 及实现
  3. multi_div.c multi_div.h: 乘除法 API 及实现

‼️ 项目涉及到的 gcc 参数:

参数描述
-c编译、汇编指定的源文件(也就是编译源文件),但是不进行链接
-o用来指定输出文件
-L为 gcc 增加一个搜索链接库的目录
-l用来指定程序要链接的库

简易计算器的编译

这一章节我们将正式开始进行简易四则预算程序的编译实验,分步骤进行。

1️⃣ 主程序的编译、链接与执行

1.1 编译 -C

✅ 打开 chapter0 文件夹查看 main.c 文件,内容如下:

#include <stdio.h>

int main(void)
{
    printf("Hello Cacu!\n");
    return 0;
}

在 Terminal 中执行以下命令,对 main.c 文件只编译而不链接。

gcc -c main.c

可以发现在当前目录中生成了一个新的文件 main.o

图片.png

通过file命令查看main.o的文件格式

file main.o 

会打印出 log:“main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped”

这说明 main.o 实际上是一个 relocatable(重定位) object 文件。

通过以下命令为 main.o 文件赋予可执行的权限:

chmod 777 main.o

输入以下命令尝试执行 main.o 文件:

./main.o

Terminal 输出可执行文件格式错误,如图所示:

图片.png

说明 relocatable object 文件是不可执行的。


1.2 链接 -O

接下来通过 GCC 对 main.o 文件进行链接操作,从而生成一个可执行的程序 main

在 Terminal 中输入以下命令将 main.o 链接为 main 文件:

gcc -o main main.o

可以发现当前目录新增了一个名为 main 的文件。

图片.png

通过 file 命令查看 main 的文件格式:

file main.o

会打印出 log:“main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=3753fcc57530a2eb08e63879f8363013bef5d161, not stripped” 此时文件类型已经变更为 ` "executable" 。

执行此文件:

./main

输出结果:

图片.png

可以看到有log印出“Hello Cacu!”。 这正是我们main.c里希望打印的语句,说明文件被正常执行。

感兴趣的同学也可以使用readelf工具查看main文件的更多细节。


2️⃣ 静态链接

2.1 文化编写编译

编写 add_minus.h 文件,在文件中对函数 add()minus() 进行声明:

#ifndef __ADD_MINUS_H__
#define __ADD_MINUS_H__

int add(int a, int b);
int minus(int a, int b);

#endif /*__ADD_MINUS_H__*/
  • 添加 add_minus.c 文件,实现 add()minus()
#include "add_minus.h"

int add(int a, int b)
{
    return a+b;
}

int minus(int a, int b)
{
    return a-b;
}

add_minus.c 文件进行编译,生成 add_minus.o 文件。

gcc -c add_minus.c

2.2 patch 打补丁

修改 main.c 文件,为其增加加减法运算并编译这个文件。

执行以下命令给 main.c 打上 v1.0.patch 补丁:

patch -p2 < v1.0.patch

patch 命令可以处理 diff 程序生成的补丁文件,补丁格式可以是四种比较格式中任意一种, 然后把这些差异融入到原始文件中,生成一个打过补丁的版本。
就是更新文件。-p 选项表示剥离层级.
通过在 Terminal 中输入 man patch 命令可获取详细说明。

详细文章 Linux patch命令

打完 patchmain.c 内容如下:

#include <stdio.h>
#include "add_minus.h"

int main(void)
{
    int rst;

    printf("Hello Cacu!\n");

    rst = add(3,2);
    printf("3 + 2 = %d\n",rst);

    rst = minus(3,2);
    printf("3 - 2 = %d\n",rst);

    return 0;
}

通过以下命令对 main.c 文件进行编译和链接:

gcc -c main.c
gcc -o main main.o

链接生成的 main.o 文件时,发现有错误出现,错误内容如图所示:

图片.png

原因在于链接过程中找不到 addminus 这两个 symbol。

main.oadd_minus.o 链接成可执行文件并执行测试:

gcc -o main main.o add_minus.o

执行新生成的可执行文件 main

./main

输出结果如下:

图片.png

说明程序得到了正常执行。

重新编译 add_minus.c 生成 add_minus.o 文件。

gcc -c add_minus.c

2.3 ar 打包到静态库

通过 ar 命令将 add_minus.o 打包到静态库中。

ar rc libadd_minus.a add_minus.o

-r  将文件插入备存文件中。 c  建立备存文件

详细介绍:Linux ar命令

可以发现在当前目录下,生成了一个名为 libadd_minus.a 的静态库文件。

图片.png

file 命令查看 libadd_minus.a 的文件格式。

file libadd_minus.a

图片.png

实际上 libxxx.a 格式的文件可以简单的看成指定的以 .o 结尾的文件集合。

2.4 链接 main.o 和静态库文件。

gcc -o main2 main.o -L./ -ladd_minus

-L./:表明库文件位置在当前文件夹。

-ladd_minus:表示链接 libadd_minus.a 文件,使用 -l 参数时,前缀 lib 和后缀 .a 是需要省略的。

执行 main2:

./main2

图片.png

说明程序得到了正确的执行。


3️⃣ 动态链接

  • 添加multi_div.h文件,声明 multi()div()
#ifndef __MULTI_DIV_H__
#define __MULTI_DIV_H__

int multi(int a, int b);
int div(int a, int b);

#endif /*__MULTI_DIV_H__*/
  • 添加multi_div.c文件,实现 multi() div()
#include "multi_div.h"

int multi(int a, int b)
{
    return a*b;
}

int div(int a, int b)
{
    return a/b;
}
  • multi_div.c 文件编译成动态链接库。
gcc multi_div.c -fPIC -shared -o libmulti_div.so

-fPIC 选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码。

inux编译动态库之fPIC

来看看吧 ! 命令执行结束后,在当前目录下会生成一个名为 libmulti_div.so 的文件。

图片.png

通过 file 命令来查看 libmulti_div.so 的文件格式。

file libmulti_div.so

可得到文件格式:“libmulti_div.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2334680eed2923cb153d687fd0605d320f7fb8a2, not stripped” 即表明 libmulti_div.so 是一个 shared object 文件。

删除之前的 main.c 文件,并编写新的 main.c 文件,内容如下:

#include <stdio.h>

int main(void)
{
    printf("Hello Cacu!\n");
    return 0;
}
图片.png

通过以下命令为 main.c 打上 v2.0.patch 补丁:

patch -p2 < v2.0.patch

此时main.c中的内容为:

#include <stdio.h>
/*
#include "add_minus.h"
*/
#include "multi_div.h"

int main(void)
{
    int rst;

    printf("Hello Cacu!\n");
/*
    rst = add(3,2);
    printf("3 + 2 = %d\n",rst);

    rst = minus(3,2);
    printf("3 - 2 = %d\n",rst);
*/
    rst = multi(3,2);
    printf("3 * 2 = %d\n",rst);

    rst = div(6,2);
    printf("6 / 2 = %d\n",rst);

    return 0;
}

编译 main.c 生成 main.o

gcc -c main.c

链接 main.o 与动态链接库文件。

gcc -o main3 main.o -L./ -lmulti_div

执行 main3 文件:

./main3

会打印错误:“./main3: error while loading shared libraries: libmulti_div.so: cannot open shared object file: No such file or directory”

打印错误🆘(动态库不在库文件搜索路径)

出现错误的原因是我们生成的动态库 libmulti_div.so 并不在库文件搜索路径中。

解决方法可以二选一:

    1. libmulti_div.so 拷贝到 /lib//usr/lib/ 文件夹下。
sudo cp libmulti_div.so /usr/lib
    1. LD_LIBRARY_PATH 变量中指定库文件路径,而动态链接库文件存放在 /home/project/make_example-master/chapter0/ 路径下。所以需要在 Terminal 中执行下面的命令:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/project/make_example-master/chapter0/

更改后

现在在 Terminal 中执行下面的命令:

./main3

图片.png

说明程序得到了正确的执行。


静态\动态链接库🈁区别

静态和动态的区别就是在链接时如何处理库函数\color{red}{如何处理库函数},将它加入到应用程序中。

  • 静态链接,是在编译的时候加入到程序中\color{red}{编译的时候加入到程序中}
  • 动态链接,是在运行时加载DLL的可执行代码加入到程序中。\color{red}{加载DLL的可执行代码加入到程序中。}
  • (所以无论是动态链接库还是静态链接库,都会有lib文件)
  • 链接指的是重定位代码使其可执行。\color{red}{重定位代码使其可执行。}
  • 运行生成可执行文件EXE\color{red}{生成可执行文件EXE。}
  • 运行时需要加载DLL信息,使其可执行,因此DLL文件需要和exe文件放到同一目录下

参考文章: 动态链接库和静态链接库的区别

尽管我们知道无论是静态链接还是动态链接都能达到链接对象文件生成可执行文件的目的,但是我们还是得 z 注意静态链接库与动态链接库之间的区别,详细内容参考 Static, Shared Dynamic and Loadable Linux Libraries


4️⃣ 混合使用静态链接与动态链接

删除旧的 main.c 文件,并编写新的 main.c 文件,内容如下:

#include <stdio.h>

int main(void)
{
    printf("Hello Cacu!\n");
    return 0;
}

为新的 main.c 文件打上 v3.0.patch 补丁。

patch -p2 < v3.0.patch

编译 main.c 生成 main.o

gcc -c main.c

测试执行混用静态链接和动态链接的方式。

gcc -o main4 main.o -L./ -ladd_minus -lmulti_div

由于我们之前已经修改过 LD_LIBRARY_PATH 变量,所以此次无需再次修改。

执行下面的命令:

./main4

输出结果如下:

图片.png

说明程序得到正确的执行。


🔚总结思考

本实验说明了 GCC 基本编译,链接的方法。 在修改和测试代码的过程中需要反复执行编译和链接动作,由此产生基本的自动化编译需求。

课后习题

1.二者的链接优先级

  1. 请思考和验证若静态库和动态库名称关键字相同,如: 静态库名称:libxxx.a 动态库名称:libxxx.so 二者的链接优先级如何?如何指定链接其中之一?

动态库和静态库的使用方法是一样的,同一库如果同时存在动态库和静态库,优先链接动态库\color{red}{同一库如果同时存在动态库和静态库,优先链接动态库},除非使用--static强制使用静态库。
库标准路径下存在libABC.a和libABC.so使用gcc -lABC如何选择连接静态连接库或者动态连接库?通过--hare --static选项?如果在同一路径下面,并且两种库同名,这样会选择动态库。


2. 编写shell自动编译c语言脚本

  1. 请按照本课程的实验步骤自行编写 script 进行自动编译.

#!/bin/bash
filename=$1
location=$(pwd)
cd $location
# echo $location
gcc $filename -o ${filename/c/out}
./${filename/c/out}

参考文章:C语言自动编译执行脚本
注意,创建软硬链接要先进入root模式才有权限
还有要绝对路径:使用ln 命令使用绝对路径就可以了
ln -s /home/shiyanlou/Code/mygcc /usr/local/bin/

最后使用截图:

图片.png


3. shell优缺点

  1. 并思考用 script 的优点和缺陷.参考文章:shell优缺点


待续