GCC使用入门

news2024/9/29 11:53:53

文章目录

  • GCC简介
  • 单个文件编译过程
    • 预处理(Preprocessing)
    • 编译(Compilation)
    • 汇编(Assembly)
    • 链接(Linking)
  • 多文件编译过程
    • 头文件搜索路径
      • 三种不推荐的方法
      • 两种推荐的方法
  • 库文件
    • 静态库文件
      • 创建和使用静态库
      • 链接顺序
    • 动态库文件
      • 创建和使用动态库
  • Warning编译选项
  • 调试信息(-g)
  • 编译优化
  • 总结
  • 参考文献

GCC简介

gcc(GNU Compiler Collection,GNU编译器套件)是一个功能强大、跨平台的编译器套件,用于编译C、C++、Objective-C、Fortran、Ada、Go和D等多种编程语言的源代码。它是GNU项目的一部分,遵循GPL(GNU General Public License)许可证,因此可以自由使用和分发。gcc主要有如下特点:

  • 1、跨平台:gcc可以在多种操作系统上运行,包括Linux、Windows(通过MinGW)等。
  • 2、优化:gcc提供了多种优化选项,可以根据需要选择不同级别的优化来生成更高效率的代码。
  • 3、可扩展性:gcc支持通过插件和扩展来增加新功能或支持新的编程语言。
  • 4、开源
  • 5、多语言支持:支持C、C++、Objective-C、Fortran、Ada、Go和D等语言的编译。
  • 6、丰富的选项:gcc提供了大量的编译选项,允许开发者精细地控制编译过程,包括预处理、编译、汇编和链接等各个阶段。
  • 7、调试支持:gcc生成的代码可以与多种调试器(如gdb)配合使用,帮助开发者定位和解决程序中的问题。
  • 8、文档和社区支持:gcc拥有详细的文档和广泛的社区支持。

本文主要是介绍gcc的入门使用,gcc的安装教程请参考其他文章。

检验gcc是否安装可以使用gcc --version 。如果内容是类似下面的格式,则说明已经安装了gcc。

zld@zld:~$ gcc --version
gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

单个文件编译过程

对于经典的"Hello, World"程序:

zld@zld:~/Codes/tmp0926$ cat -n hello.c 
     1  #include <stdio.h>
     2
     3  int main()
     4  {
     5          printf("Hello, World\n");
     6          return 0;
     7  }

使用gcc编译单个文件非常简单,只需要使用命令gcc <源文件>即可,比如用gcc编译上面的hello.c,则用命令gcc hello.c就能编译成功,gcc会生成一个a.out的可执行文件(当然也可以通过参数-o来指定生成的文件名)

zld@zld:~/Codes/tmp0926$ gcc hello.c 
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x  2 zld zld  4096 Sep 27 11:16 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:16 a.out*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./a.out 
Hello, World
# 通过 -o 参数生成可执行文件
zld@zld:~/Codes/tmp0926$ gcc hello.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x  2 zld zld  4096 Sep 27 11:22 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:16 a.out*
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:22 hello*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./hello 
Hello, World

上面的编译看似很简单,但其实这个过程可以分为4个阶段,分别是预处理(preprocessing)编译(compilation)汇编(assembly)链接(linking)

默认情况下,gcc编译不会保存中间结果,我们可以通过选项-save-temps来保存中间结果。如下所示,当加了-save-temps选项后,除了生成最终的可执行文件a.out外,还有以下几个文件a-hello.ia-hello.sa-hello.o这其实就是上面几个阶段生成的中间结果。其中,.i文件是预处理之后产生的结果,.s是编译后产生的结果,.o是汇编产生的结果。

zld@zld:~/Codes/tmp0926$ gcc hello.c -save-temps
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x  2 zld zld  4096 Sep 27 11:28 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:28 a-hello.i
-rw-rw-r--  1 zld zld  1496 Sep 27 11:28 a-hello.o
-rw-rw-r--  1 zld zld   659 Sep 27 11:28 a-hello.s
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:28 a.out*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c

因此,对于上述hello.c文件,用gcc编译的过程可以用下图说明:
在这里插入图片描述

下面将结合hello.c源程序详细讲述这4个阶段的处理过程。

预处理(Preprocessing)

gcc编译过程的第一阶段就是预处理,预处理过程主要处理那些源代码文件中的以"#“开始的预编译指令。比如”#include"、"#define"等,主要处理规则如下:

  • 将所有的"#define"删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如"#if"、“#ifdef”、“#elif”、“#else”、“#endif”。
  • 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有的注释"//“和”/* */"。
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。

经过预处理之后,生成的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。

预处理阶段是用预处理器cpp程序处理的,可以用以下命令处理预处理过程。(-E表示只进行预处理)。

cpp hello.c > hello.i
或者
gcc -E hello.c -o hello.i

对上述的hello.c程序进行预处理:

zld@zld:~/Codes/tmp0926$ ll
total 12
drwxrwxr-x  2 zld zld 4096 Sep 27 11:39 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ gcc -E hello.c -o hello1.i
zld@zld:~/Codes/tmp0926$ cpp hello.c > hello2.i
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x  2 zld zld  4096 Sep 27 11:44 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r--  1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c

预处理之后的结果如下:

zld@zld:~/Codes/tmp0926$ cat hello1.i
...
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 967 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main()
{
 printf("Hello, World\n");
 return 0;
}

对于上述的例子可能还不好理解,我们再举一个例子。

对于下面的一段程序test1.c

zld@zld:~/Codes/tmp0926$ cat -n test1.c 
     1  #define TEST "hello,world"
     2
     3  /* This is a comments */
     4  const char str[] = TEST;

我们的预处理结果如下:

zld@zld:~/Codes/tmp0926$ cpp test1.c > test1.i
zld@zld:~/Codes/tmp0926$ ll
total 60
drwxrwxr-x  2 zld zld  4096 Sep 27 12:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r--  1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c
-rw-rw-r--  1 zld zld   165 Sep 27 12:11 test1.i
zld@zld:~/Codes/tmp0926$ cat test1.i 
# 0 "test1.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "test1.c"



const char str[] = "hello,world";

从上面的结果可以看到,源程序test1.c的第1行的宏定义#define TEST "hello,world"以及第3行的注释/* This is a comments */已经被删除了,并且在源程序的第四行 const char str[] = TEST;已经将该宏定义展开了。

编译(Compilation)

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件(简单来说,编译就是将预处理之后的源代码翻译成汇编代码)。这是整个构建过程的核心部分。对于编译过程,gcc使用的是ccl程序来完成的。

编译过程相当于如下命令(-S表示执行编译后停止,不进行汇编和链接):

gcc -S hello.i -o hello.s
或者
gcc -S hello.c -o hello.s

对于hello.c程序,执行结果如下:

zld@zld:~/Codes/tmp0926$ gcc -S hello.i -o hello.s
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x  2 zld zld  4096 Sep 27 12:24 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld   659 Sep 27 12:24 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

如果想看详细过程,还可以加上选项-v,它表示显示gcc执行时的详细过程。
在这里插入图片描述

从上面可以看到在编译过程实际是由ccl程序执行的,因此也可以直接用ccl程序执行:

zld@zld:~/Codes/tmp0926$ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 hello.i 
 main
Analyzing compilation unit
Performing interprocedural optimizations
 <*free_lang_data> {heap 920k} <visibility> {heap 920k} <build_ssa_passes> {heap 920k} <opt_local_passes> {heap 1224k} <remove_symbols> {heap 1224k} <targetclone> {heap 1224k} <free-fnsummary> {heap 1224k}Streaming LTO
 <whole-program> {heap 1224k} <fnsummary> {heap 1224k} <inline> {heap 1224k} <modref> {heap 1224k} <free-fnsummary> {heap 1224k} <single-use> {heap 1224k} <comdats> {heap 1224k}Assembling functions:
 <simdclone> {heap 1224k} main
Time variable                                   usr           sys          wall           GGC
 phase setup                        :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)  1819k ( 80%)
 phase parsing                      :   0.00 (  0%)   0.01 (100%)   0.01 ( 33%)   403k ( 18%)
 phase opt and generate             :   0.01 (100%)   0.00 (  0%)   0.01 ( 33%)    60k (  3%)
 callgraph optimization             :   0.01 (100%)   0.00 (  0%)   0.00 (  0%)     0  (  0%)
 callgraph ipa passes               :   0.01 (100%)   0.00 (  0%)   0.00 (  0%)  4880  (  0%)
 preprocessing                      :   0.00 (  0%)   0.01 (100%)   0.00 (  0%)    35k (  2%)
 parser (global)                    :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)   351k ( 15%)
 final                              :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)  2120  (  0%)
 TOTAL                              :   0.01          0.01          0.03         2283k

汇编(Assembly)

汇编器就是将编译生成的汇编代码(.s)转变成机器可以执行的指令,在Linux系统上一般表现为ELF目标文件(OBJ文件)(简单来说,就是将汇编代码翻译成机器码),用到的汇编器工具为as。

汇编过程用到的命令如下:

gcc -c hello.s -o hello.o
或者
as hello.s -o hello.o

对于hello.c程序,执行结果如下:

zld@zld:~/Codes/tmp0926$ gcc -c hello.s -o hello.o
zld@zld:~/Codes/tmp0926$ as hello.s -o hello2.o
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x  2 zld zld  4096 Sep 27 12:51 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello2.o
-rw-rw-r--  1 zld zld   659 Sep 27 12:27 hello2.s
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello.o
-rw-rw-r--  1 zld zld   474 Sep 27 12:40 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

详细执行过程:
在这里插入图片描述

链接(Linking)

链接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为ld或collect2。

链接的命令如下:

zld@zld:~/Codes/tmp0926$ gcc hello.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 68
drwxrwxr-x  2 zld zld  4096 Sep 27 13:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15880 Sep 27 13:11 hello*
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello2.o
-rw-rw-r--  1 zld zld   659 Sep 27 12:27 hello2.s
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld  1376 Sep 27 12:52 hello.o
-rw-rw-r--  1 zld zld   474 Sep 27 12:40 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

其详细执行过程:
在这里插入图片描述

多文件编译过程

上一章节是讲的单个源文件的编译过程。在一个实际的项目中,一般情况下是包含多个源文件的。本章将介绍多个文件的编译过程。

我们首先将上一章的hello.c程序改造一下:该目录下有三个文件分别是hello.chello.hmain.c,其中,hello.c中定义了一个函数hello(),其功能就是简单的打印字符串。hello.h头文件中就是声明了hello()函数。在main.c中则是定义了main()函数,在main函数中调用了hello.c文件中的hello()函数。

zld@zld:~/Codes/tmp0926$ tree
.
├── hello.c
├── hello.h
└── main.c


zld@zld:~/Codes/tmp0926$ cat hello.c 
#include <stdio.h>
#include "hello.h"

void hello(const char *string)
{
        printf("%s\n", string);
}

zld@zld:~/Codes/tmp0926$ cat hello.h 
void hello(const char *string);


zld@zld:~/Codes/tmp0926$ cat main.c 
#include <stdio.h>
#include "hello.h"

int main()
{
        hello("hello,world");
        return 0;
}

对于多个源文件的编译,有两种方式,第1种就是将多个源程序一起编译生成可执行程序;第2种方法就是分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。

第1种方法:将多个源程序一起编译生成可执行程序。

zld@zld:~/Codes/tmp0926$ gcc hello.c main.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x  2 zld zld  4096 Sep 27 13:48 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 16016 Sep 27 13:48 hello*
-rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c

第2种方法:分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。

zld@zld:~/Codes/tmp0926$ gcc -c hello.c -o hello.o
zld@zld:~/Codes/tmp0926$ gcc -c main.c -o main.o
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x  2 zld zld 4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld   32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld 1360 Sep 27 13:49 hello.o
-rw-rw-r--  1 zld zld   88 Sep 27 13:33 main.c
-rw-rw-r--  1 zld zld 1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ gcc hello.o main.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x  2 zld zld  4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 16016 Sep 27 13:50 hello*
-rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld  1360 Sep 27 13:49 hello.o
-rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c
-rw-rw-r--  1 zld zld  1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ ./hello 
hello,world

这里有以下几个问题值得讨论一下:

  • 问题1、在上面的方法1或者方法2中,都没有用到头文件hello.h,为什么?

在编译过程中,不需要用到头文件,这是因为在预处理步骤中,预处理的程序会将"#include"中包含的文件插入该预处理指令的位置。

  • 问题2、在预处理处理"#include"指令时,程序是在哪个目录查找到头文件的?

    对于该问题,先卖个关子,在后面的例子中再讲。

  • 问题3、在第2种方法中,当将多个目标文件链接再一起时,对目标文件的顺序有没有要求?

    比如上面是gcc hello.o main.o -o hello,那能不能按照gcc main.o hello.o -o hello的顺序呢?

    我们实践一下:

    zld@zld:~/Codes/tmp0926$ gcc main.o hello.o  -o hello2
    zld@zld:~/Codes/tmp0926$ ll
    total 60
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:06 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    -rwxrwxr-x  1 zld zld 16016 Sep 27 13:50 hello*
    -rwxrwxr-x  1 zld zld 16016 Sep 27 14:06 hello2*
    -rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
    -rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
    -rw-rw-r--  1 zld zld  1360 Sep 27 13:49 hello.o
    -rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c
    -rw-rw-r--  1 zld zld  1488 Sep 27 13:50 main.o
    

    实践证明,gcc对目标文件的链接顺序是没有要求。之所以讲这个,是因为有些老的链接器如果顺序不对,则有可能报“undefined references”错误,如果调整一下链接顺序,则该错误又没有了。但是现在的编译器和链接器一般是不需要考虑这个顺序的,GCC就不需要考虑。

    但是,我们后面会看到,如果链接过程中有静态库文件和目标文件,则有顺序要求。

通常来说,编译所消耗的时间要比链接所消耗的时间要长,因此,如果把所有的程序都写到一个文件中,那只要有修改,则需要重新编译和链接,对于一个大型的程序来说,这是非常耗时的。但如果分成了多个文件,那我们可以只对修改的文件进行编译,然后再将编译后生成的目标文件和之前没有修改的文件的目标文件再一起链接即可,这样就能减少整个构建的时间。

头文件搜索路径

上面的例子是将3个文件(main.c、hello.c、hello.h)放到同一个目录中的。但有过实际项目的人都知道,一般不会将所有文件放到同一个目录中的,而是比如头文件放到注入include的目录中,库文件放到诸如lib的目录中,然后源文件也会基于各自的功能分别创建各自的目录的。

我们稍微改一下上面3个文件的目录。目录结构如下。将hello.c文件放到了hello目录中,将hello.h放到include目录中,而main.c和hello文件夹以及include文件夹在同一层中。

zld@zld:~/Codes/tmp0926$ tree
.
├── hello
│   └── hello.c
├── include
│   └── hello.h
└── main.c

3 directories, 3 files

此时,我们按照上面的方法1再编译一下(这里注意一下,我是在当前main.c文件所处的目录编译的,所以hello.c的相对位置就是hello/hello.c):

zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello1
main.c:2:10: fatal error: hello.h: No such file or directory
    2 | #include "hello.h"
      |          ^~~~~~~~~
compilation terminated.
hello/hello.c:2:10: fatal error: hello.h: No such file or directory
    2 | #include "hello.h"
      |          ^~~~~~~~~
compilation terminated.

从上面可知,编译报错了,错误原因是找不到hello.h头文件。为什么会找不到头文件呢?我们用-v选项来看下详细信息:

zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -v -o hello1
...
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/13/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
Compiler executable checksum: edbc28f9c9bb85637ee0b8e5b79ac141
main.c:2:10: fatal error: hello.h: No such file or directory
    2 | #include "hello.h"
      |          ^~~~~~~~~
compilation terminated.

我只显示了关键的一些信息,从上面可以看到当搜索#include “…” 时是在当前目录下查找#include "..." search starts here:。在搜索#include <...>是在以下路径查找(下面的这些路径称为系统路径):

#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/13/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include

很显然,"hello.h"既不在当前路径下,也不在系统路径上,所以找不到而报错。

既然知道报错的原因了,那就知道怎么解决问题了。

三种不推荐的方法

  • 方法1:在#include中写出头文件hello.h的绝对路径。如下所示:

    zld@zld:~/Codes/tmp0926/hello$ cat hello.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "/home/zld/Codes/tmp0926/include/hello.h"
    
    zld@zld:~/Codes/tmp0926$ cat main.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "/home/zld/Codes/tmp0926/include/hello.h"
    

    修改完成后,再次编译,这次就编译成功了,而且运行正确:

    zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello2
    zld@zld:~/Codes/tmp0926$ ll
    total 36
    drwxrwxr-x  4 zld zld  4096 Sep 27 16:49 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    drwxrwxr-x  2 zld zld  4096 Sep 27 16:47 hello/
    -rwxrwxr-x  1 zld zld 16016 Sep 27 16:49 hello2*
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:31 include/
    -rw-rw-r--  1 zld zld   142 Sep 27 16:46 main.c
    zld@zld:~/Codes/tmp0926$ ./hello2 
    hello,world
    

    这种方式非常不推荐

    • 1、可移植性差
      • 绝对路径是特定于一个具体的文件系统布局的。如果代码被移植到其他系统或环境,文件系统的结构可能不同,导致编译器无法找到头文件。
      • 这使得代码在不同开发者之间、不同机器之间或不同项目之间的共享和复用变得困难。
    • 2、维护困难。
      • 如果头文件的位置发生变化,则所有包含绝对路径的源代码文件都需要更新。
      • 这增加了维护负担,尤其是在大型项目中,头文件的位置可能会频繁变动。
  • 方法2:在#include中写出头文件hello.h的相对路径。如下所示:

    zld@zld:~/Codes/tmp0926/hello$ cat hello.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "../include/hello.h"
    
    zld@zld:~/Codes/tmp0926$ cat main.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "include/hello.h"
    

    运行结果:

    zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello3
    zld@zld:~/Codes/tmp0926$ ll
    total 52
    drwxrwxr-x  4 zld zld  4096 Sep 27 17:37 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    drwxrwxr-x  2 zld zld  4096 Sep 27 17:36 hello/
    -rwxrwxr-x  1 zld zld 16016 Sep 27 16:49 hello2*
    -rwxrwxr-x  1 zld zld 16016 Sep 27 17:37 hello3*
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:31 include/
    -rw-rw-r--  1 zld zld   118 Sep 27 17:35 main.c
    zld@zld:~/Codes/tmp0926$ ./hello3 
    hello,world
    

    这种方法比方法1要好一些,如果各个文件的相对位置不变,则也不用花费许多功夫维护(实际上,有些开源代码上有这么写)。但是如果相对位置一变,就需要修改对应的include。因此,这种方法本人也是不推荐的。

  • 方法3:将头文件放到上面所说的系统路径中,然后在写#include语句时,就可以不想相对或绝对路径了:

    zld@zld:~/Codes/tmp0926/include$ cp hello.h /usr/local/include/
    cp: cannot create regular file '/usr/local/include/hello.h': Permission denied
    zld@zld:~/Codes/tmp0926/include$ su
    Password: 
    root@zld:/home/zld/Codes/tmp0926/include# cp hello.h /usr/local/include/
    
    root@zld:/home/zld/Codes/tmp0926/hello# cat hello.c 
    #include <stdio.h>
    #include "hello.h"
    
    root@zld:/home/zld/Codes/tmp0926# cat main.c 
    #include <stdio.h>
    #include "hello.h"
    

    运行结果:

    root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello4
    root@zld:/home/zld/Codes/tmp0926# ./hello4
    hello,world
    

    但是这也有一些问题。比如,从上面可以知道,我将头文件拷贝到系统路径时,必须要root权限。此外,如果将所有的头文件都拷贝到系统路径,那这个系统路径下将包含各种各样的头文件,这样这个目录下将变得非常的杂乱不堪。因此,也是不推荐这种方法。

两种推荐的方法

上面的3种方法,我们都不太满意,那有什么更好的办法呢?我们想一下,如果我们告诉编译器去哪个路径去找头文件,那是不是事情就解决了?

有两种方法可以解决这个事情:

  • 1、设置C头文件的搜索路径的环境变量。

    export C_INCLUDE_PATH=/home/zld/zld/codes/0926:$C_INCLUDE_PATH
    

    将上面的例子,用该方法编译一下,编译过程如下:

    # 设置C语言头文件的环境变量
    root@zld:/home/zld/Codes/tmp0926# export C_INCLUDE_PATH=$C_INCLUDE_PATH:/home/zld/Codes/tmp0926/include
    root@zld:/home/zld/Codes/tmp0926/include# env | grep C_INCLUDE
    C_INCLUDE_PATH=:/home/zld/Codes/tmp0926/include
    
    #重新编译与运行(注意:我已经将方法3中拷贝到系统路径下的hello.h文件已经删除了,防止影响)
    root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello5
    root@zld:/home/zld/Codes/tmp0926# ./hello5 
    hello,world
    
  • 2、在gcc命令中加上-Idir选项,其中dir是头文件的路径,I是字母i的大小,并且I与dir之间没有空格。

# -I加上绝对路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I/home/zld/Codes/tmp0926/include -o hello6
root@zld:/home/zld/Codes/tmp0926# ./hello6
hello,world

# -I加上相对路径:点'.'表示当前路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I. -o hello7

上述两个方法比之前的三种方法,要方便得多。因此推荐这两种方法。在这两种方法中,在命令中加上-I选项更加常用。因为如果换到其他的环境中,那还得重新设置一下环境变量。但是命令行中加上-I选项一般可以在makefile文件中都已经写好了,因此更加方便。

对于添加环境变量的方法,对于C++来说,是用CPLUS_INCLUDE_PATH,对于静态库文件来说是用LIBRARY_PATH,对于动态库文件来说是LD_LIBRARY_PATH。至于什么是静/动态库文件,本文后面会讲到。

这里做一下总结:

# C头文件搜索路径环境变量设置
C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz
export C_INCLUDE_PATH

# C++头文件搜索路径环境变量设置
CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz
export CPLUS_INCLUDE_PATH

# 动态库搜索路径环境变量设置
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib
export LD_LIBRARY_PATH

# 静态库搜索路径环境变量设置
LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib
export LIBRARY_PATH

同样,对于gcc命令中,可以加-L来添加库文件路径。

# 在gcc命令中添加 -Idir 选项来添加头文件搜索路径
# 在gcc命令中添加 -Ldir 选项来添加库文件搜索路径

库文件

请大家思考一个问题,在hello.c程序中,我们调用了printf()函数,但是我们实际上并不知道这个函数是怎么实现的,我们只是#include <stdio.h>

zld@zld:~/Codes/tmp0926$ cat -n hello.c 
     1  #include <stdio.h>
     2
     3  int main()
     4  {
     5          printf("Hello, World\n");
     6          return 0;
     7  }

假设还有另外一个场景:假设你是一个公司的老板,你们公司开发了一个非常牛B的模块,这时候有客户希望单独将该模块提供给他们使用。作为公司的老板,你自然是不希望将该模块的源代码提供给客户。

上面的两个场景都涉及到库(Library)文件。把库文件和头文件给到对方就可以达到提供功能又不暴露源码的目的了。

库文件可以分成静态库文件和动态库文件。在Linux下,静态库文件是以.a后缀结束的文件。动态库文件是以.so后缀结束的文件。

静态库文件

什么是静态库文件呢?我举一个例子。假设当前你有4个文件:add.csub.cmulti.cdiv.c。从名字也可以看出来,它们分别实现了加法、减法、乘法、除法的功能。并且将这4个源文件都生成了对应的目标文件:add.osub.omulti.odiv.o。我们前面说过,如果其他程序要引用这4个文件,那么可以在gcc命令生成可执行程序时,加上这4个目标文件即可。加上4个文件还好,但是如果有非常多的文件呢?这样一个个添加似乎不太合理,效率也不太高。另外一方面,加减乘除这四个文件,其实可以统称为对数字的运算嘛。能不能将这4个文件像压缩包一样压缩成一个文件呢?可以,静态库文件就是这样干的。

通俗的说,静态库(Static Library)文件就是一个打包了多个目标文件(.o文件)的归档(archive)文件。打个比方就是,目标文件相当于图书馆里面的书。而静态库文件就是图书馆(我想这就是为啥库文件英文是Library了)。

静态库文件有以下特点:

  • 1.编译时链接:静态库在编译时被链接到最终的可执行文件中,这意味着库中的代码和数据会被直接复制到可执行文件中。
  • 2.独立性:由于静态库在编译时就被整合到可执行文件中,因此生成的可执行文件是独立的,不依赖于任何外部的库文件。这使得静态库编译的程序具有更好的可移植性和部署简便性。
  • 3.重复代码整合:如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这增加了可执行文件的大小,但同时确保了每个程序都有完整的代码副本,无需担心其他程序对库代码的修改或删除。
  • 4.性能考虑:静态库在编译时就被整合到可执行文件中,因此没有运行时加载库的开销。这可能在某些性能敏感的应用中是一个优势。
  • 5.更新和维护挑战:如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序。这可能会是一个繁琐的过程,特别是在大型项目或涉及多个依赖库的情况下。

创建和使用静态库

在Linux中,可以用ar命令来创建静态库:

ar cr libNAME.a file1.o file2.o ... filen.o

其中,其中cr是选项,c表示create,r表示replace。ar其实就是archiver的缩小。

通过该命令,最终创建出一个静态库文件libNAME.a

我们还可以使用以下命令查看一个静态库文件中包含的目标文件列表:

ar t libNAME.a

举例

假设有如下4个文件,func1.c中定义了一个函数func1(),它的功能是返回两个整型数相加的值。func2.c中定义了func2()函数,该函数的功能是打印给定的数字。在main.c中分别调用了func1函数和func2函数。在mylib.h中定义了两个函数的声明。我们试图将func1.cfunc2.c的程序生成一个静态库文件,然后共main.c使用。

zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x  2 zld zld 4096 Sep 27 22:07 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h

zld@zld:~/Codes/tmp0926$ cat func1.c 
#include "mylib.h"

int func1(int x, int y)
{
        return (x+y);
}
zld@zld:~/Codes/tmp0926$ cat func2.c 
#include <stdio.h>
#include "mylib.h"

void func2(int x)
{
        printf("The result is %d\n", x);
}
zld@zld:~/Codes/tmp0926$ cat main.c 
#include <stdio.h>
#include "mylib.h"

int main()
{
        int i;
        i = func1(1,2);
        func2(i);
        return 0;
}
zld@zld:~/Codes/tmp0926$ cat mylib.h 
int func1(int x, int y);
void func2(int x);

首先,将func1.cfunc2.c文件分别生成两个目标文件(.o):

# -Wall是告警选项,本文后面会讲到,如果你还不了解,可以去掉这个选项
zld@zld:~/Codes/tmp0926$ gcc -Wall func1.c -c
zld@zld:~/Codes/tmp0926$ gcc -Wall func2.c -c
zld@zld:~/Codes/tmp0926$ ll
total 32
drwxrwxr-x  2 zld zld 4096 Sep 27 22:14 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h

然后,将func1.ofunc2.o两个目标文件生成一个hello的静态库文件:

# 生成静态库文件
zld@zld:~/Codes/tmp0926$ ar cr libhello.a func1.o func2.o
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x  2 zld zld 4096 Sep 27 22:16 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r--  1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h

# 查看静态库文件libhello.a包含的目标文件
zld@zld:~/Codes/tmp0926$ ar t libhello.a 
func1.o
func2.o

这样,我们就生成了静态库文件libhello.a

最后,生成最终的可执行文件并运行:

zld@zld:~/Codes/tmp0926$ gcc -Wall main.c libhello.a -o hello
zld@zld:~/Codes/tmp0926$ ./hello 
The result is 3

# 也可以先生成main.o文件,然后再和静态库文件链接生成最终可执行文件
zld@zld:~/Codes/tmp0926$ gcc main.o libhello.a -o hello2
zld@zld:~/Codes/tmp0926$ ./hello2
The result is 3

上述命令我们是通过libNAME的方式来链接的,我们还可以通过-lNAME的方式来链接,因此,也可以用以下的命令实现:

zld@zld:~/Codes/tmp0926$ gcc -Wall main.o -lhello -o hello6
/usr/bin/ld: cannot find -lhello: No such file or directory
collect2: error: ld returned 1 exit status

我们发现,命令行竟然执行失败了,说找不到-lhello,也就是说,找不到libhello.a。明明我们当前目录下有libhello.a,那为啥找不到呢?这是因为通过-lhello的方式是在系统目录下去找,而系统目录下没有该静态库文件,因此找不到该hello库文件。那怎么办呢?前面我们说过有三种方法:

  • 1、Command-line options ‘-I’ and ‘-L’, from left to right.在命令行中加上-I或者-L选项,指明头文件或者库文件的目录。其中-I是指明头文件的目录,-L是指明库文件的目录。

  • 2、设置环境变量。Directories specified by environment variables, such as C_INCLUDE_PATH and

    # C头文件搜索路径环境变量设置
    C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz
    export C_INCLUDE_PATH
    
    # C++头文件搜索路径环境变量设置
    CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz
    export CPLUS_INCLUDE_PATH
    
    # 动态库搜索路径环境变量设置
    LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib
    export LD_LIBRARY_PATH
    
    # 静态库搜索路径环境变量设置
    LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib
    export LIBRARY_PATH
    
  • 3、将头文件或者库文件放到系统目录中。

前面我们在头文件的搜索过程说过不推荐使用方法3,并且在那里也有示例,因此这里我们就不再对方法3进行举例了。我们只对方法1和方法2进行举例。

我们使用第1种方法:

zld@zld:~/Codes/tmp0926$ gcc main.o -L. -lhello -o h6
zld@zld:~/Codes/tmp0926$ ll
total 120
drwxrwxr-x  2 zld zld  4096 Sep 27 23:09 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

在上面的命令gcc main.o -L. -lhello -o h6中,-L.表示指定搜索库库文件目录为当前目录(所以用的是点号.),然后指定外部库为-lhello

从上面的执行结果可以看到,成功生成了可执行文件h6。

-L也可以用绝对路径:

zld@zld:~/Codes/tmp0926$ gcc main.o -L/home/zld//Codes/tmp0926 -lhello -o h7
zld@zld:~/Codes/tmp0926$ ll
total 136
drwxrwxr-x  2 zld zld  4096 Sep 27 23:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

下面使用第2种方法:

# 首先创建环境变量
zld@zld:~/Codes/tmp0926$  env | grep LIB
zld@zld:~/Codes/tmp0926$ pwd
/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LIBRARY_PATH=/home/zld/Codes/tmp0926:$LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ env | grep LIB
LIBRARY_PATH=/home/zld/Codes/tmp0926:

# 执行命令
zld@zld:~/Codes/tmp0926$ gcc main.o -lhello -o h8
zld@zld:~/Codes/tmp0926$ ll
total 152
drwxrwxr-x  2 zld zld  4096 Sep 27 23:14 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:14 h8*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

链接顺序

我们前面说过,将几个目标文件进行链接时,各个目标文件的位置在哪是没有关系的,比如下面的几种链接顺序都是可以生成可执行程序的:

zld@zld:~/Codes/tmp0926$ gcc main.o func1.o func2.o -o hello3
zld@zld:~/Codes/tmp0926$ ./hello3 
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func1.o func2.o main.o -o hello4
zld@zld:~/Codes/tmp0926$ ./hello4
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func2.o func1.o main.o -o hello5
zld@zld:~/Codes/tmp0926$ ./hello5
The result is 3

那我们之前的命令:

gcc -Wall main.c libhello.a -o hello6
或者
gcc main.o libhello.a -o hello7

可以将main.c(或者main.o)与libhello.a的顺序对调吗?也就是说,下面的命令会执行正确吗?

gcc -Wall libhello.a main.c -o hello
gcc libhello.a main.o -o hello7

我们运行一下,发现两条命令都报错了:

zld@zld:~/Codes/tmp0926$ gcc -Wall libhello.a main.c -o hello
/usr/bin/ld: /tmp/ccxCA1TA.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status

zld@zld:~/Codes/tmp0926$ gcc libhello.a main.o -o hello7
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status

链接器的主要任务之一就是解析符号引用,即将目标文件和库文件中的未解析符号(如函数和变量)与其他文件或库中的已定义符号进行匹配。链接器按照命令中指定的顺序依次处理每个目标文件和库文件。如果被依赖的目标文件(或库)在引用它的目标文件(或库)之前被处理,链接器将无法找到并解析这些符号引用,因为它们还未被加入到符号集合中。这有可能导致undefined reference错误。

在上面的命令gcc libhello.a main.o -o hello7中,gcc是按照从左到右的顺序依次处理的,这样被依赖的库libhello.a先被处理,而引用它的目标文件main.o后被处理,因此就无法解析func1func2函数了。

动态库文件

和静态库对应的还有动态库。既然已经有了静态库,那为什么还要动态库呢?这是由于静态库的特点导致的。我们前面说过,如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这样会导致内存空间的浪费。并且如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序,这样会导致难以维护和更新。

正是由于静态库的这些缺点,引入了动态库。

动态库文件,也称为动态库链接(Dynamic Link Library,简称DLL)在Windows系统上,或在Linux和Unix系统上称为共享对象库(Shared Object Library,简称so),是一种包含可被多个程序同时使用的代码和数据的库文件。

使用动态库可以节省内存空间,因为多个程序可以共享同一个库文件中的代码,而不需要在每个程序的可执行文件中都包含一份副本。此外,它还有助于软件更新,因为只需要更新一个库文件,而不必重新编译所有依赖该库的应用程序。

动态库的主要特点包括:

  • 1.共享性:多个应用程序可以同时使用同一个动态库中的函数和资源,这有助于减少系统的整体内存占用。
  • 2.可更新性:如果动态库中的错误被修复或者功能得到增强,只需要替换掉旧版本的库文件,无需重新编译或重新安装依赖此库的所有应用程序。
  • 3.模块化:动态库支持将大型应用程序分解为更小、更易于管理的部分,这有利于团队合作开发和维护。

下面总结一下动态库文件和静态库文件的区别:

  • 1、链接时机:动态库文件在程序运行时被加载到内存中,而静态库文件在程序编译链接时被整合到可执行文件中。
  • 2、文件大小与磁盘空间:动态库文件不会增加可执行文件的大小,多个程序可以共享一个动态库文件,节省磁盘空间。静态库文件会增加可执行文件的大小,因为静态库的代码被完全复制到了可执行文件中,如果多个程序使用同一个静态库,会造成存储资源的浪费。
  • 3、运行时依赖:动态库文件编译的程序在运行时需要外部库文件的支持,如果动态库缺失或版本不匹配,程序可能无法正常运行。静态库文件编译的程序在运行时不需要外部库文件的支持,因为它们已经包含了所有必要的代码和数据。
  • 4、更新与维护:动态库文件可以独立于程序进行更新,当需要更新时,只需要替换掉旧的动态库文件,无需重新编译链接依赖于它的所有程序。静态库文件更新时需要重新编译链接所有依赖该库的程序,以确保所有程序都使用更新后的代码。
  • 5、性能与内存使用:动态库文件在程序运行时被加载,可能会增加一些加载时间,但多个程序可以共享同一个动态库文件,节省内存使用。静态库文件在程序运行时已经加载到内存中,无需额外的加载时间,但可能会导致内存使用量的增加,尤其是当多个程序使用同一个静态库时。

在实际中,动态库用得更多。

创建和使用动态库

在Linux上,动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为".so"。

可以通过gcc编译器使用-fPIC-shared选项来创建共享对象文件(.so文件)。其中-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。

gcc -fpic -shared 源文件名... -o 动态库文件

或者

先使用 gcc -c 指令将指定源文件编译为目标文件,再由目标文件生成动态链接库
gcc -c -fPIC 源文件名... -o 目标文件
gcc -shared 目标文件... -o 动态库文件

我们对之前的4个文件,将func1.cfunc2.c合并创建一个动态库文件。

zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x  2 zld zld 4096 Sep 27 23:59 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h

zld@zld:~/Codes/tmp0926$ gcc -fpic -shared func1.c func2.c -o libhello.so
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x  2 zld zld  4096 Sep 28 00:04 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rwxrwxr-x  1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

然后,像静态库链接一样,和main.c一起生成可执行文件:

zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -o h1
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x  2 zld zld  4096 Sep 28 00:10 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rwxrwxr-x  1 zld zld 15968 Sep 28 00:10 h1*
-rwxrwxr-x  1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h
zld@zld:~/Codes/tmp0926$ ./h1 
./h1: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory

发现执行可执行程序h1时失败,说无法找到动态库文件libhello.so

运行由动态库生成的可执行文件时,必须确保程序在运行时可以找到这个动态库。和静态库文件一样,可以通过添加动态库的环境变量来解决,或者在执行上述gcc命令时添加动态库路径。

# 通过添加动态库环境变量解决
zld@zld:~/Codes/tmp0926$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LD_LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ ./h1 
The result is 3


# 在执行gcc命令时添加动态库链接
zld@zld:~/Codes/tmp0926$ gcc main.c -lhello -L. -o h2
zld@zld:~/Codes/tmp0926$ ./h2 
The result is 3

zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -L. -o h3
zld@zld:~/Codes/tmp0926$ ./h3
The result is 3

同静态库文件一样,动态库文件在链接时也有顺序要求,如果将上述命令换一个顺序,则也会报错:

zld@zld:~/Codes/tmp0926$ gcc libhello.so main.c -L. -o h4
/usr/bin/ld: /tmp/ccLWWSlT.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status

Warning编译选项

我们在前面的一些例子中,增加了-Wall选项,这个选项就是一个编译告警选项。事实上,在实际的项目中,一般总是会增加告警选项的。

比如下面的一段程序中,在打印printf("MAX + MIN = %f\n", MAX + MIN);中,我们将格式化输出写成%f了(即浮点型),但是实际结果是一个整型。

[zld@localhost 0926]$ cat test.c 
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{
        printf("MAX + MIN =  %f\n", MAX + MIN);
        return 0;
}

如果不加告警选项,则不会有告警信息,并且运行结果不符合预期:

[zld@localhost 0926]$ gcc test.c -o test
[zld@localhost 0926]$ ./test 
MAX + MIN =  0.000000

因此,无论如何,都建议加上告警选项,提前识别出告警风险。

# 加上告警选项上,就有告警信息提示了
[zld@localhost 0926]$ gcc -Wall test.c -o test2
test.c: In function ‘main’:
test.c:7: warning: format ‘%f’ expects type ‘double’, but argument 2 has type ‘int’

# 根据提示,修改程序。然后告警消失
[zld@localhost 0926]$ cat test.c 
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{
        printf("MAX + MIN =  %d\n", MAX + MIN);     #这里改成%d
        return 0;
}
[zld@localhost 0926]$ gcc -Wall test.c -o test3
[zld@localhost 0926]$ ./test3 
MAX + MIN =  4      # 运行结果正确

一些常用的告警选项:

  • 1、-w(小写)禁止所有告警信息

  • 2、以**-W**(大写)开头开启特定的告警。比如

    -Wreturn-type(返回值告警),
    -Wsign-compare(有符号和无符号对比告警)
    -Wall (除extra外的所有告警)
    -Wextra (all外的其他告警)
    
    
  • 3、以“-Wno-”开头关闭特定的警告;

例如:
-Wno-return-type (取消返回值告警)
-Wno-sign-compare(取消有符号和无符号对比告警)
  • 4、将告警转变成错误。
    • -Werror :所有告警当错误报
    • -Werror= 将指定的警告转换为错误。
    • 反过来:-Wno-error取消编译选项-Werror

GCC定义了非常多的告警信息,这里就不一一列出来了,可以直接参考gcc文档。

调试信息(-g)

一般来说,之前的gcc命令生成的可执行程序都不包含调试信息,如果程序崩溃了,那么则获取崩溃的文件名以及行号。

gcc提供了-g 调试选项,这样生成的可执行程序,倘若出现问题,便可以使用 gdb 找出问题具体出现的位置,便于问题的解决。

例如,对于下面一段有问题的程序:

[zld@localhost 0926]$ cat -n null.c 
     1  int a(int *p);
     2
     3  int main()
     4  {
     5          int *p = 0;
     6          return a(p);
     7  }
     8
     9  int a(int *p)
    10  {
    11          int y = *p;
    12          return y;
    13  }

在第5行,因为p赋值为0,也就是NULL。因此在调用函数a时,在第11行对指针进行解引用时,会出错。但是这是运行时错误,因此在编译时,编译器不会报错:

[zld@localhost 0926]$ gcc -Wall -g null.c -o null
[zld@localhost 0926]$ 

但是在运行时,就会报错:

[zld@localhost 0926]$ ./null 
Segmentation fault (core dumped)

但是看当前目录,并没有产生core文件,这是因为很多操作系统默认是不产生core文件的。可以通过ulimit -c查看。如果是0,则说明不会产生core文件。

[zld@localhost 0926]$ ulimit -c
0

可以通过ulimit -c unlimited来修改可以产生core文件:

[zld@localhost 0926]$ ulimit -c unlimited
[zld@localhost 0926]$ ulimit -c
unlimited

再次运行,就产生了core文件了:

[zld@localhost 0926]$ ./null 
Segmentation fault (core dumped)
[zld@localhost 0926]$ ll
-rw-------. 1 zld zld 188416 Sep 26 03:47 core.6484

然后就可以用gdb工具来定位了:

[zld@localhost 0926]$ gdb null core.6484 
zld@zld:~/Codes/tmp0926$ gdb null core 
GNU gdb (Ubuntu 14.0.50.20230907-0ubuntu1) 14.0.50.20230907-git
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from null...
[New LWP 2738]

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.ubuntu.com>
Enable debuginfod for this session? (y or [n]) n
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./null'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000625194ea415b in a (p=0x0) at null.c:13
13                      int y = *p;
(gdb) backtrace 
#0  0x0000625194ea415b in a (p=0x0) at null.c:13
#1  0x0000625194ea4149 in main () at null.c:7

由于本文是介绍gcc的使用,因此关于gdb如何使用,本文不做过多描述。大家可以在网上搜索如何使用gdb。

编译优化

gcc 提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对{编译时间,目标文件长度,执行效率}这个三维模型进行不同的取舍和平衡。优化的方法不一而足,总体上将有以下几类:1)精简操作指令;2)尽量满足cpu的流水操作;3)通过对程序行为地猜测,重新调整代码的执行顺序;4)充分使用寄存器;5)对简单的调用进行展开等等。想全部了解这些编译选项,并在其中挑选适合的选项进行优化,无疑像个噩梦般的过程。

幸好gcc提供了从O0O3这几种不同的优化级别供大家选择。

在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。

参数 -O1-O2-O3 中,随着数字变大,代码的优化程度也越高,不过这在某种意义上来说,也是以牺牲程序的可调试性为代价的,因此优化和调试是一对矛盾体

举例:

下面一段代码:

zld@zld:~/Codes/tmp0926$ cat test.c 
#include <stdio.h>

double powern(double d, unsigned n)
{
        double x = 1.0;
        unsigned j;
        for (j=1;j<=n;j++)
        {
                x *= d;
        }
        return x;
}

int main()
{
        double sum = 0.0;
        unsigned i;
        // 循环20亿次
        for (i=1;i <=2000000000;i++)
        {
                sum += powern(i, i%5);
        }
        printf("sum = %g\n", sum);
        return 0;
}

用不同的优化级别看下运行的时间,从结果上看,随着优化级别的调高,相应的运行时间也逐渐减少。

zld@zld:~/Codes/tmp0926$ gcc -Wall -O0 test.c -o O0
zld@zld:~/Codes/tmp0926$ time ./O0 
sum = 1.28e+45

real    0m12.983s
user    0m12.963s
sys     0m0.000s


zld@zld:~/Codes/tmp0926$ gcc -Wall -O1 test.c -o O1
zld@zld:~/Codes/tmp0926$ time ./O1 
sum = 1.28e+45

real    0m3.522s
user    0m3.517s
sys     0m0.000s


zld@zld:~/Codes/tmp0926$ gcc -Wall -O2 test.c -o O2
zld@zld:~/Codes/tmp0926$ time ./O2 
sum = 1.28e+45

real    0m3.245s
user    0m3.233s
sys     0m0.004s
zld@zld:~/Codes/tmp0926$ 

zld@zld:~/Codes/tmp0926$ gcc -Wall -O3 test.c -o O3
zld@zld:~/Codes/tmp0926$ time ./O3 
sum = 1.28e+45

real    0m3.124s
user    0m3.118s
sys     0m0.000s


# 加上循环展开优化(-funroll-loop),又进一步优化了
zld@zld:~/Codes/tmp0926$ gcc -Wall -O4 -funroll-loops test.c -o O4
zld@zld:~/Codes/tmp0926$ time ./O4 
sum = 1.28e+45

real    0m2.755s
user    0m2.748s
sys     0m0.000s

我们上面说过,编译优化和调试信息(-g)通常来说是一对矛盾体。我个人的建议是要优先保证调试信息,毕竟如果没有调试信息,当程序崩溃时,我们都无法定位,这在实际工作中是一件非常麻烦的事,得加班加点了。。。。。

总结

这个章节是对前面章节用到的一些gcc命令以及选项做一个总结。

参数选项含义
-E仅执行预处理,不进行编译、汇编和链接(生成后缀为 .i 的预编译文件)
-S执行编译后停止,不进行汇编和链接(生成后缀为 .s 的预编译文件)
-c编译程序,但不链接成为可执行文件(生成后缀为 .o 的文件)
-o对输出文件命名
-O/-O1/-O2/-O3优化代码,减少代码体积,提高代码效率,但是相应的会增加编译的时间
-lNAME(这里是小写的L)指定程序要链接的库,NAME为库文件名称。可以是静态库文件,也可以是动态库文件
-Ldir指定-l(小写-L)所使用到的库文件所在路径。dir为具体的路径
-Idir(这里是大写的i)增加 include 头文件路径。dir为具体的路径
-DNAME预定义宏,NAME为相应的宏名称
-shared生成共享文件,然后可以与其它文件链接生成可执行文件
-fpic生成适用于共享库的与地址无关的代码(PIC)
-w不输出任何警告信息
-Wall开启编译器的批量告警选项
-Werror将所有的警告当成错误进行处理,在所有产生警告的地方停止编译
-g生成调试信息,方便gdb调试
-save-temps保存编译中间结果
-v显示gcc执行时的详细过程
–version显示gcc的版本信息

参考文献

1、GCC, the GNU Compiler Collection - GNU Project

2、https://blog.csdn.net/bandaoyu/article/details/115419255

3、https://blog.csdn.net/qq_31108501/article/details/51842166
4、https://www.runoob.com/w3cnote/gcc-parameter-detail.html
5、文心一言

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2176623.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快递单号物流跟踪管理快速筛选出已签收单号

看着满屏的单号&#xff0c;是不是感觉眼前一黑要查询到什么时候&#xff1f;别灰心&#xff0c;这不快递批量查询高手来了&#xff01;这神器就是用来查询物流的好帮手。一键筛选已签收件单号&#xff0c;并导出表格。有了它&#xff0c;你也能轻松查询大量的单号物流。一起试…

买前必看,教你挑选适合自己的蓝牙耳机(我早点刷到该多好啊)

无论是运动、通勤&#xff0c;还是休闲娱乐时&#xff0c;蓝牙耳机已经成为我们便捷生活的一部分。那么&#xff0c;市场上这么多款蓝牙耳机&#xff0c;我们究竟该怎么选&#xff1f;耳机挑不对&#xff0c;买了也白费&#xff01;买蓝牙耳机之前要搞清楚耳机的这些参数&#…

【算法业务】关于数据驱动的用户增长思考

这篇内容是多年之前&#xff08;2020年&#xff09;的用户增长项目时自己写的总结&#xff0c;这里做一下对于实践和思考的回顾&#xff0c;便于知识的记录和经验分享&#xff0c;内容涉及用户增长理解、个性化推送系统框架、个性化推送问题建模、推送内容池构建、智能文案生成…

BMT Building Maker Toolset 房屋建筑快速创建工具

BuildingMakerToolset提供了一个用于创建建筑和放置预制件的自定义工作流程。 如果你需要为你的游戏设计一些带室内装饰的建筑,或者你是一名关卡设计师,你想让你的工作流程更有效,这可能是适合你的资产。 该工具集与200多个墙壁、电缆、管道等预制件配对。所有预制件都指定了…

基于NXP LS1046+FPGA的轨道交通3U CPCI多网口解决方案,支持QNX/VXWOKRS/LINUX

Feature Summary Specification Description 处理器 NXP LS1046A at up to 1.4GHz 存储 DDR4&#xff0c; 16GB Emmc&#xff0c;16MB QSPI FLASH 板卡形状 3U标准CPCI板卡 尺寸 160.00 100.00mm 接口 2路2.5GE 2路1GE 1路RS232 1路IRIGB 调试接口 JTAG / COP de…

AI生成头像表情包副业,每天仅需十分钟,无脑操作月入过万!

项目介绍 今天我想与大家分享一个有趣的项目&#xff1a;AI生成表情包和头像。这对于我们进行IP打造来说&#xff0c;实在是个不错的选择&#xff0c;尤其是像我这样的头像。那为什么说每天只需花费10分钟呢&#xff1f;接下来我们来探讨一下。 这个项目的核心在于利用AI技术…

读取到json数据拿出来,修改后重新写入json文件

在写程序过程中&#xff0c;有些时候需要拿到json里面的数据&#xff0c;再进行修改&#xff0c;哪该怎么操作呢&#xff1f;跟着我以下的操作进行&#xff0c;就能更改json文件的内容了。 比如说我要修改年级的状态&#xff0c;修改为0 先创建一个json文件&#xff0c;数据格…

vue3项目中引入Cesium

1、创建项目 本文章是我学习Cesium时记录下来的&#xff0c;是我用来学习使用的。 使用vitevue3创建项目&#xff0c;组件库使用element plus&#xff0c;项目地址在我的gitee仓库中有&#xff0c;https://gitee.com/the-world-keeps-blooming/my-vite-vue-cesium。 在vite中有…

高效修复MySQL数据库

介绍 MySQL被广泛认为是最著名的数据库管理系统之一&#xff0c;是跨各种行业的许多应用的基础。mysql数据库的耐用性和效率是决定这些应用程序是否能不受任何干扰地运行的重要因素。需要对MySQL数据库进行定期维护&#xff0c;以防止发生以下情况:数据丢失和系统中断。此外&a…

springboot购物网站源码分享

开头&#xff1a;springboot购物网站源码分享 题目&#xff1a;springboot购物网站源码分享 主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Mysql|大数据|SSM|SpringBoot|Vue|Jsp|MYSQL等)、学习资料、JAVA源码、技术咨询 文末联系获取 感兴趣可以先收藏起来&#xff…

YOLOv8改进,YOLOv8主干网络替换为GhostNetV3(2024年华为提出的轻量化架构,全网首发),助力涨点

摘要 GhostNetV3 是由华为诺亚方舟实验室的团队发布的,于2024年4月发布。 摘要:紧凑型神经网络专为边缘设备上的应用设计,具备更快的推理速度,但性能相对适中。然而,紧凑型模型的训练策略目前借鉴自传统模型,这忽略了它们在模型容量上的差异,可能阻碍紧凑型模型的性能…

大数据-152 Apache Druid 集群模式 配置启动【下篇】 超详细!

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

【H2O2|全栈】关于CSS(9)CSS3扩充了哪些新鲜的东西?(二)

目录 CSS3入门 前言 准备工作 伪元素补充 :before :after 文本溢出属性 转换效果 预告和回顾 后话 CSS3入门 前言 本系列博客主要介绍CSS相关的知识点。 这一期主要介绍以下几个CSS3的知识点&#xff1a; 伪元素补充文本溢出属性转换 没有基础的朋友&#xff…

大堆对象是如何影响程序的性能的

在本文中&#xff0c;我们将详细了解 JVM 如何存储对象及其在内存中的表示形式。此外&#xff0c;我们将深入探讨性能影响以及如何利用它们来获得优势。 *此外&#xff0c;我们将了解如何使用-XX:UseCompressedOops以及它如何影响应用程序的性能。此外&#xff0c;我们将了解U…

[大语言模型-论文精读] 阿里巴巴-通过多阶段对比学习实现通用文本嵌入

[大语言模型-论文精读] 阿里巴巴达摩院-GTE-通过多阶段对比学习实现通用文本嵌入 1. 论文信息 这篇论文《Towards General Text Embeddings with Multi-stage Contrastive Learning》介绍了一种新的文本嵌入模型&#xff0c;名为GTE&#xff08;General-purpose Text Embeddin…

低空经济时代:无人机飞行安全要点详解

随着低空经济的蓬勃发展&#xff0c;无人机&#xff08;UAV&#xff09;在农业、航拍、物流、应急救援等多个领域的应用日益广泛。然而&#xff0c;无人机的安全飞行不仅关乎任务的成功与否&#xff0c;更直接关系到地面人员、财产及空中交通的安全。本文将从飞行前检查、环境评…

大数据-153 Apache Druid 案例 从 Kafka 中加载数据并分析

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

【Linux学习】【Ubuntu入门】1-2 新建虚拟机ubuntu环境

1.双击打开VMware软件&#xff0c;点击“创建新的虚拟机”&#xff0c;在弹出的中选择“自定义&#xff08;高级&#xff09;” 2.点击下一步&#xff0c;自动识别ubuntu光盘映像文件&#xff0c;也可以点击“浏览”手动选择&#xff0c;点击下一步 3.设置名称及密码后&#xf…

1Panel安装部署证书(httpsok.com)

1Panel安装部署证书(httpsok.com) 购买服务器 推荐购买香港服务器&#xff0c;这样通过域名访问就不需要备案。 创建静态站点 申请SSL证书 进入 httpsok.com&#xff0c;点击申请证书 输入站点域名 根据提示&#xff0c;添加DNS解析记录 添加成功后&#xff0c;提示域名验证…

如何在AI绘画SD中调节光照?这2个超好用的方法别错过!轻松生成AI人像光感大片!

大家好&#xff0c;我是画画的小强 在AI绘画Stable Diffusion 摄影艺术中&#xff0c;灯光的运用对于照片的质量和情感表达至关重要。它不仅能够彰显主题&#xff0c;还能为画面增添深度与立体感&#xff0c;帮助传递感情&#xff0c;以及凸显细节之美。 下面&#xff0c;我将…