【Linux操作系统】GCC编译与静态库、动态库制作详解

GCC是一款广泛使用的开源编译器,它支持多种编程语言,并且具有强大的编译能力。在软件开发中,我们经常需要将代码编译成可执行文件或者库文件。本文将详细介绍GCC编译过程以及如何制作静态库和动态库。

文章目录

一、GCC编译过程

GCC编译过程主要分为四个阶段:预处理、编译、汇编和链接 。下面我们将逐一介绍每个阶段的作用。

1. 预处理阶段

预处理阶段主要是对源代码进行宏展开、头文件包含、条件编译等预处理操作。预处理器会根据源文件中的预处理指令,生成一个新的文件,通常以.i作为扩展名。

示例代码:

c 复制代码
// main.c
#include <stdio.h>

#define PI 3.1415926

int main() {
    printf("PI = %f\n", PI);
    return 0;
}

预处理后的代码:

c 复制代码
// main.i
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
...

2. 编译阶段

编译阶段将预处理后的代码转换成汇编代码,即将高级语言代码翻译成汇编语言代码。编译器会检查语法错误、类型错误等,并生成一个汇编文件,通常以.s作为扩展名。

示例代码:

c 复制代码
// main.i
# 1 "main.c"
...
int main() {
    printf("PI = %f\n", PI);
    return 0;
}

编译后的汇编代码:

assembly 复制代码
// main.s
    .file "main.c"
    .section    .rodata
.LC0:
    .string "PI = %f\n"
    .text
.globl main
    .type   main, @function
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movsd   .LC0(%rip), %xmm0
    movl    $1, %eax
    movl    $.LC1, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .section    .rodata
.LC1:
    .string "PI = %f\n"
    .text
    .section    .note.GNU-stack,"",@progbits

3. 汇编阶段

汇编阶段将汇编代码转换成机器代码。汇编器会将汇编代码转换成二进制指令,生成一个目标文件,通常以.o作为扩展名。

示例代码:

assembly 复制代码
// main.s
    .file "main.c"
...

汇编后的目标文件:

object 复制代码
// main.o
...

4. 链接阶段

链接阶段将目标文件与所需的库文件进行链接,生成最终的可执行文件。链接器会解析目标文件中的符号引用,并将其与库文件中的符号定义进行匹配。

示例代码:

object 复制代码
// main.o
...

链接后的可执行文件:

executable 复制代码
// main
...

二、静态库制作

好的,下面我将给出一个更详细的例子来说明如何制作和使用静态库。

1. 静态库制作

首先,我们需要创建两个源文件add.c和sub.c,分别实现加法和减法的功能。

add.c:

c 复制代码
int add(int a, int b) {
    return a + b;
}

sub.c:

c 复制代码
int sub(int a, int b) {
    return a - b;
}

然后,我们使用gcc命令将这两个源文件编译成目标文件。

shell 复制代码
$ gcc -c add.c sub.c

生成add.o和sub.o文件。

接下来,我们使用ar命令将目标文件打包成一个静态库文件libmath.a。

shell 复制代码
$ ar rcs libmath.a add.o sub.o

这一步完之后你的目录里会包含
add.c /sub.c /add.o /sub.o /libmath.a

2. 使用静态库

现在我们已经制作好了一个静态库libmath.a,接下来我们将使用这个静态库。

首先,我们创建一个main.c文件,调用静态库中的函数。

main.c:

c 复制代码
#include <stdio.h>

extern int add(int a, int b);
extern int sub(int a, int b);

int main() {
    int a = 10;
    int b = 5;
    
    int sum = add(a, b);
    int difference = sub(a, b);
    
    printf("Sum: %d\n", sum);
    printf("Difference: %d\n", difference);
    
    return 0;
}

要进行声明函数,要不系统会默认隐形声明,容易导致错误。

然后,我们使用gcc命令将main.c文件与静态库链接起来生成可执行文件。

shell 复制代码
$ gcc main.c -L. -lmath -o main

最后,我们运行可执行文件main。

shell 复制代码
$ ./main

输出结果:

makefile 复制代码
Sum: 15
Difference: 5

补充:头文件对应

假设我们有一个名为example的静态库,其中包含了一个函数void print_hello()。为了使用这个函数,我们需要有一个头文件example.h,其中包含了函数的声明。

example.h:

c 复制代码
#ifndef EXAMPLE_H
#define EXAMPLE_H

void print_hello();

#endif

在使用这个静态库的源文件中,我们需要包含example.h头文件,并调用其中的函数。

main.c:

c 复制代码
#include <stdio.h>
#include "example.h"

int main() {
    printf("Hello, world!\n");
    print_hello();
    return 0;
}

在编译时,我们需要指定静态库文件的搜索路径和要链接的库文件。假设libexample.a是我们的静态库文件,可以使用以下命令进行编译:

shell 复制代码
gcc -o program main.c -L/path/to/library -lexample

其中-L/path/to/library指定了静态库文件的搜索路径,-lexample指定要链接的静态库。

总结一下:首先,我们需要将多个目标文件打包成一个静态库文件,然后在编译时指定静态库的路径和名称。最后,我们可以通过调用静态库中的函数来使用其中的功能。希望这个例子能够帮助你更好地理解静态库的制作和使用过程。

三、动态库制作

动态库是在程序运行时被加载的库文件,它可以被多个程序共享使用,减少了内存的占用。

1. 动态库制作

首先,我们需要创建两个源文件add.c和sub.c,分别实现加法和减法的功能。

add.c:

c 复制代码
int add(int a, int b) {
    return a + b;
}

sub.c:

c 复制代码
int sub(int a, int b) {
    return a - b;
}

然后,我们使用gcc命令将这两个源文件编译成目标文件,并使用-fPIC选项生成位置无关的代码。

shell 复制代码
$ gcc -c -fPIC add.c sub.c

同样生成add.o和sub.o文件。

接下来,我们使用gcc命令将目标文件打包成一个动态库文件libmath.so

shell 复制代码
$ gcc -shared -o libmath.so add.o sub.o

2. 使用动态库

现在我们已经制作好了一个动态库libmath.so,接下来我们将使用这个动态库。

首先,我们创建一个main.c文件,调用动态库中的函数。

main.c:

c 复制代码
#include <stdio.h>
#include <dlfcn.h>

int main() {
    void* handle = dlopen("./libmath.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Failed to open library: %s\n", dlerror());
        return 1;
    }

    int (*add)(int, int) = dlsym(handle, "add");
    int (*sub)(int, int) = dlsym(handle, "sub");

    int a = 10;
    int b = 5;

    int sum = add(a, b);
    int difference = sub(a, b);

    printf("Sum: %d\n", sum);
    printf("Difference: %d\n", difference);

    dlclose(handle);

    return 0;
}
/*
首先,在main函数中,我们声明了一个void指针变量handle,用于存储打开动态库后返回的句柄。然后,我们使用dlopen函数打开动态库文件libmath.so,指定RTLD_LAZY标志表示在需要时才解析符号。如果打开动态库失败,我们使用dlerror函数获取错误信息并打印到stderr流上,然后返回1表示出错。

接下来,我们使用dlsym函数获取动态库中的函数指针。dlsym函数的第一个参数是动态库的句柄,第二个参数是要获取的函数名。我们使用函数指针的方式来声明和初始化两个函数指针变量add和sub,分别指向动态库中的add函数和sub函数。

然后,我们定义了两个整型变量a和b,并分别赋值为10和5。

接下来,我们通过调用函数指针变量add和sub来调用动态库中的函数,得到加法和减法的结果,并分别赋值给sum和difference变量。

最后,我们使用dlclose函数关闭动态库句柄,释放资源。


*/

然后,我们使用gcc命令将main.c文件与动态库链接起来生成可执行文件,并指定动态库的路径和名称。

shell 复制代码
$ gcc main.c -L. -ldl -o main

最后,我们运行可执行文件main。

shell 复制代码
$ ./main

输出结果:

makefile 复制代码
Sum: 15
Difference: 5

总结一下:首先,我们需要将多个目标文件编译成位置无关的代码,并使用gcc命令将它们打包成一个动态库文件。然后,在使用动态库的程序中,我们需要使用dlopen函数打开动态库,并使用dlsym函数获取动态库中的函数指针。最后,我们可以通过调用动态库中的函数来使用其中的功能。

补充:动态库加载错误及解决方法

如果动态库路径错误,可以按照以下方法解决:

  1. 检查动态库文件的路径是否正确:确保指定的路径是动态库文件所在的准确路径。

  2. 使用绝对路径或相对路径:可以使用绝对路径来指定动态库的路径,例如/path/to/library/libexample.so。或者,使用相对路径来指定动态库的路径,相对路径是相对于当前工作目录的路径,例如./libexample.so

  3. 设置LD_LIBRARY_PATH环境变量:可以通过设置LD_LIBRARY_PATH环境变量来指定动态库的搜索路径。例如,如果动态库文件在/path/to/library目录中,可以执行以下命令:

    shell 复制代码
    export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH

    这将把/path/to/library添加到动态库的搜索路径中。

  4. 使用rpath选项:可以在链接时使用-rpath选项来指定动态库的搜索路径。例如,使用以下命令来编译和链接程序:

    shell 复制代码
    gcc -o program main.c -L/path/to/library -Wl,-rpath=/path/to/library -lexample

    这将在程序中设置动态库的搜索路径为/path/to/library

通过以上方法,您可以解决动态库路径错误的问题。请注意,在使用LD_LIBRARY_PATH环境变量或rpath选项时,确保指定的路径是正确的,并且动态库文件存在于该路径中。

四、总结

本文详细介绍了GCC编译过程以及如何制作静态库和动态库。通过预处理、编译、汇编和链接四个阶段,我们可以将源代码转换成可执行文件或者库文件。静态库将多个目标文件打包成一个文件,程序在编译时会将静态库的代码复制到可执行文件中;而动态库是在程序运行时被加载的库文件,它可以被多个程序共享使用,减少了内存的占用。

内容补充:


补充:gcc使用技巧

gcc是GNU Compiler Collection(GNU编译器套件)的缩写,是一个广泛使用的编程语言编译器。它支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada和Go等。下面是gcc的常用参数和其作用的简要说明:

  • -c:只编译源文件,生成目标文件(.o文件),不进行链接操作。
  • -o:指定输出文件的名称。
  • -I:指定头文件的搜索路径。
  • -L:指定库文件的搜索路径。
  • -l:链接时使用的库文件。
  • -g:生成调试信息,用于调试程序。
  • -Wall:开启所有警告信息。
  • -Werror:将警告视为错误。
  • -std:指定使用的C或C++标准。
  • -O:优化级别,包括-O0(无优化)、-O1(基本优化)、-O2(更多优化)和-O3(最大优化)等。
  • -shared:生成一个共享库文件(动态库)。
  • -fPIC:生成位置无关的代码,用于生成动态库。
  • -pthread:链接多线程库。
相关推荐
jjyangyou3 小时前
物联网核心安全系列——物联网安全需求
物联网·算法·安全·嵌入式·产品经理·硬件·产品设计
憧憬一下19 小时前
Pinctrl子系统中Pincontroller和client驱动程序的编写
arm开发·嵌入式·c/c++·linux驱动开发
蓝天居士19 小时前
ES8388 —— 带耳机放大器的低功耗立体声音频编解码器(4)
嵌入式·音频·es8388
田三番21 小时前
使用 vscode 简单配置 ESP32 连接 Wi-Fi 每日定时发送 HTTP 和 HTTPS 请求
单片机·物联网·http·https·嵌入式·esp32·sntp
启明智显1 天前
AI笔筒操作说明及应用场景
人工智能·嵌入式硬件·嵌入式·ai大模型·启明智显·esp32-s3
FreakStudio1 天前
全网最适合入门的面向对象编程教程:58 Python字符串与序列化-序列化Web对象的定义与实现
python·单片机·嵌入式·面向对象·电子diy
Projectsauron5 天前
【STM32】通过 DWT 实现毫秒级延时
stm32·嵌入式·dwt
云中双月6 天前
如何使用Ida Pro和Core Dump文件定位崩溃位置(Linux下无调试符号的进程专享)
linux·嵌入式·gdb·调试·gcc·崩溃·ida pro·ulimit·core dump·cross compile
L_Z_J_I7 天前
超子物联网HAL库笔记:准备篇
笔记·物联网·嵌入式
飞凌嵌入式7 天前
FET113i-S核心板已支持RISC-V,打造国产化降本的更优解 -飞凌嵌入式
嵌入式硬件·嵌入式·risc-v·飞凌嵌入式