标签 编译 下的文章

通过理解解释、即时编译和预先编译之间的区别,有效地使用它们。

Java 是一种跨平台的编程语言。程序源代码会被编译为 字节码 bytecode ,然后字节码在运行时被转换为 机器码 machine code 解释器 interpreter 在物理机器上模拟出的抽象计算机上执行字节码指令。 即时 just-in-time (JIT)编译发生在运行期,而 预先 ahead-of-time (AOT)编译发生在构建期。

本文将说明解释器、JIT 和 AOT 分别何时起作用,以及如何在 JIT 和 AOT 之间权衡。

源代码、字节码、机器码

应用程序通常是由 C、C++ 或 Java 等编程语言编写。用这些高级编程语言编写的指令集合称为源代码。源代码是人类可读的。要在目标机器上执行它,需要将源代码转换为机器可读的机器码。这个转换工作通常是由 编译器 compiler 来完成的。

然而,在 Java 中,源代码首先被转换为一种中间形式,称为字节码。字节码是平台无关的,所以 Java 被称为平台无关编程语言。Java 编译器 javac 将源代码转换为字节码。然后解释器解释执行字节码。

下面是一个简单的 Java 程序, Hello.java

//Hello.java
public class Hello {
    public static void main(String[] args) {
         System.out.println("Inside Hello World!");
    }
}

使用 javac 编译它,生成包含字节码的 Hello.class 文件。

$ javac Hello.java
$ ls
Hello.class  Hello.java

现在,使用 javap 来反汇编 Hello.class 文件的内容。使用 javap 时如果不指定任何选项,它将打印基本信息,包括编译这个 .class 文件的源文件、包名称、公共和受保护字段以及类的方法。

$ javap Hello.class
Compiled from "Hello.java"
public class Hello {
    public Hello();
    public static void main(java.lang.String[]);
}

要查看 .class 文件中的字节码内容,使用 -c 选项:

$ javap -c Hello.class
Compiled from "Hello.java"
public class Hello {
  public Hello();
        Code:
           0: aload_0
           1: invokespecial #1                      // Method java/lang/Object."<init>":()V
           4: return

  public static void main(java.lang.String[]);
        Code:
           0: getstatic         #2                      // Field java/lang/System.out:Ljava/io/PrintStream;
           3: ldc               #3                      // String Inside Hello World!
           5: invokevirtual #4                      // Method    
java/io/PrintStream.println:(Ljava/lang/String;)V
           8: return
}

要获取更详细的信息,使用 -v 选项:

$ javap -v Hello.class

解释器,JIT 和 AOT

解释器负责在物理机器上模拟出的抽象计算机上执行字节码指令。当使用 javac 编译源代码,然后使用 java 执行时,解释器在程序运行时运行并完成它的目标。

$ javac Hello.java
$ java Hello
Inside Hello World!

JIT 编译器也在运行期发挥作用。当解释器解释 Java 程序时,另一个称为运行时 分析器 profiler 的组件将静默地监视程序的执行,统计各部分代码被解释的次数。基于这些统计信息可以检测出程序的 热点 hotspot ,即那些经常被解释的代码。一旦代码被解释次数超过设定的阈值,它们满足被 JIT 编译器直接转换为机器码的条件。所以 JIT 编译器也被称为分析优化的编译器。从字节码到机器码的转换是在程序运行过程中进行的,因此称为即时编译。JIT 减少了解释器将同一组指令模拟为机器码的负担。

AOT 编译器在构建期编译代码。在构建时将需要频繁解释和 JIT 编译的代码直接编译为机器码可以缩短 Java 虚拟机 Java Virtual Machine (JVM) 的 预热 warm-up 时间。(LCTT 译注:Java 程序启动后首先字节码被解释执行,此时执行效率较低。等到程序运行了足够的时间后,代码热点被检测出来,JIT 开始发挥作用,程序运行效率提升。JIT 发挥作用之前的过程就是预热。)AOT 是在 Java 9 中引入的一个实验性特性。jaotc 使用 Graal 编译器(它本身也是用 Java 编写的)来实现 AOT 编译。

Hello.java 为例:

//Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Inside Hello World!");
    }
}


$ javac Hello.java
$ jaotc --output libHello.so Hello.class
$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libHello.so Hello
Inside Hello World!

解释和编译发生的时机

下面通过例子来展示 Java 在什么时候使用解释器,以及 JIT 和 AOT 何时参与进来。这里有一个简单的程序 Demo.java :

//Demo.java
public class Demo {
    public int square(int i) throws Exception {
        return(i*i);
    }


    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 10; i++) {
            System.out.println("call " + Integer.valueOf(i));
            long a = System.nanoTime();
            Int r = new Demo().square(i);
            System.out.println("Square(i) = " + r);
            long b = System.nanoTime();
            System.out.println("elapsed= " + (b-a));
            System.out.println("--------------------------------");
        }
    }
}

在这个程序的 main() 方法中创建了一个 Demo 对象的实例,并调用该实例的 square()方法,然后显示 for 循环迭代变量的平方值。编译并运行它:

$ javac Demo.java
$ java Demo
1 iteration
Square(i) = 1
Time taken= 8432439
--------------------------------
2 iteration
Square(i) = 4
Time taken= 54631
--------------------------------
.
.
.
--------------------------------
10 iteration
Square(i) = 100
Time taken= 66498
--------------------------------

上面的结果是由谁产生的呢?是解释器,JIT 还是 AOT?在目前的情况下,它完全是通过解释产生的。我是怎么得出这个结论的呢?只有代码被解释的次数必须超过某个阈值时,这些热点代码片段才会被加入 JIT 编译队列。只有这时,JIT 编译才会发挥作用。使用以下命令查看 JDK 11 中的该阈值:

$ java -XX:+PrintFlagsFinal -version | grep CompileThreshold
 intx CompileThreshold     = 10000                                      {pd product} {default}
[...]
openjdk version "11.0.13" 2021-10-19
OpenJDK Runtime Environment 18.9 (build 11.0.13+8)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8, mixed mode, sharing)

上面的输出表明,一段代码被解释 10,000 次才符合 JIT 编译的条件。这个阈值是否可以手动调整呢?是否有 JVM 标志可以指示出方法是否被 JIT 编译了呢?答案是肯定的,而且有多种方式可以达到这个目的。

使用 -XX:+PrintCompilation 选项可以查看一个方法是否被 JIT 编译。除此之外,使用 -Xbatch 标志可以提高输出的可读性。如果解释和 JIT 同时发生,-Xbatch 可以帮助区分两者的输出。使用这些标志如下:

$ java -Xbatch  -XX:+PrintCompilation  Demo
         34        1        b  3           java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
         35        2         n 0           jdk.internal.misc.Unsafe::getObjectVolatile (native)   
         35        3        b  3           java.lang.Object::<init> (1 bytes)
[...]
        210  269         n 0           java.lang.reflect.Array::newArray (native)   (static)
        211  270        b  3           java.lang.String::substring (58 bytes)
[...]
--------------------------------
10 iteration
Square(i) = 100
Time taken= 50150
--------------------------------

注意,上面命令的实际输出太长了,这里我只是截取了一部分。输出很长的原因是除了 Demo 程序的代码外,JDK 内部类的函数也被编译了。由于我的重点是 Demo.java 代码,我希望排除内部包的函数来简化输出。通过选项 -XX:CompileCommandFile 可以禁用内部类的 JIT:

$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Demo

在选项 -XX:CompileCommandFile 指定的文件 hotspot_compiler 中包含了要排除的包:

$ cat hotspot_compiler
quiet
exclude java/* *
exclude jdk/* *
exclude sun/* *

第一行的 quiet 告诉 JVM 不要输出任何关于被排除类的内容。用 -XX:CompileThreshold 将 JIT 阈值设置为 5。这意味着在解释 5 次之后,就会进行 JIT 编译:

$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
-XX:CompileThreshold=5 Demo
        47      1       n 0     java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)   
           (static)
        47      2       n 0     java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)   
        47      3       n 0     java.lang.invoke.MethodHandle::linkToSpecial(LLLLLLL)L (native)   
           (static)
        48      4       n 0     java.lang.invoke.MethodHandle::linkToStatic(L)I (native)   (static)
        48      5       n 0     java.lang.invoke.MethodHandle::invokeBasic()I (native)   
        48      6       n 0     java.lang.invoke.MethodHandle::linkToSpecial(LL)I (native)   
           (static)
[...]
        1 iteration
        69   40         n 0     java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)   
           (static)
[...]
Square(i) = 1
        78   48         n 0     java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native)   
(static)
        79   49         n 0     java.lang.invoke.MethodHandle::invokeBasic(ILIJ)I (native)   
[...]
        86   54         n 0     java.lang.invoke.MethodHandle::invokeBasic(J)L (native)   
        87   55         n 0     java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)   
(static)
Time taken= 8962738
--------------------------------
2 iteration
Square(i) = 4
Time taken= 26759
--------------------------------

10 iteration
Square(i) = 100
Time taken= 26492
--------------------------------

好像输出结果跟只用解释时并没有什么区别。根据 Oracle 的文档,这是因为只有禁用 TieredCompilation-XX:CompileThreshold 才会生效:

$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \
-XX:-TieredCompilation -XX:CompileThreshold=5 Demo
124     1       n       java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)   (static)
127     2       n       java.lang.invoke.MethodHandle::invokeBasic(LLLLL)L (native)   
[...]
1 iteration
        187   40        n       java.lang.invoke.MethodHandle::linkToStatic(ILIIL)I (native)   (static)
[...]
(native)   (static)
        212   54        n       java.lang.invoke.MethodHandle::invokeBasic(J)L (native)   
        212   55        n       java.lang.invoke.MethodHandle::linkToSpecial(LJL)L (native)   (static)
Time taken= 12337415
[...]
--------------------------------
4 iteration
Square(i) = 16
Time taken= 37183
--------------------------------
5 iteration
        214   56        b       Demo::<init> (5 bytes)
        215   57        b       Demo::square (16 bytes)
Square(i) = 25
Time taken= 983002
--------------------------------
6 iteration
Square(i) = 36
Time taken= 81589
[...]
10 iteration
Square(i) = 100
Time taken= 52393

可以看到在第五次迭代之后,代码片段被 JIT 编译了:

--------------------------------
5 iteration
        214   56        b       Demo::<init> (5 bytes)
        215   57        b       Demo::square (16 bytes)
Square(i) = 25
Time taken= 983002
--------------------------------

可以看到,与 square() 方法一起,构造方法也被 JIT 编译了。在 for 循环中调用 square() 之前要先构造 Demo 实例,所以构造方法的解释次数同样达到 JIT 编译阈值。这个例子说明了在解释发生之后何时 JIT 会介入。

要查看编译后的代码,需要使用 -XX:+PrintAssembly 标志,该标志仅在库路径中有反汇编器时才起作用。对于 OpenJDK,使用 hsdis 作为反汇编器。下载合适版本的反汇编程序库,在本例中是 hsdis-amd64.so,并将其放在 Java_HOME/lib/server 目录下。使用时还需要在 -XX:+PrintAssembly 之前增加 -XX:+UnlockDiagnosticVMOptions 选项。否则,JVM 会给你一个警告。

完整命令如下:

$ java -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler \ -XX:-TieredCompilation -XX:CompileThreshold=5 -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly Demo
[...]
5 iteration
        178   56        b       Demo::<init> (5 bytes)
Compiled method (c2)    178   56                Demo::<init> (5 bytes)
 total in heap  [0x00007fd4d08dad10,0x00007fd4d08dafe0] = 720
 relocation     [0x00007fd4d08dae88,0x00007fd4d08daea0] = 24
[...]
 handler table  [0x00007fd4d08dafc8,0x00007fd4d08dafe0] = 24
[...]
 dependencies   [0x00007fd4d08db3c0,0x00007fd4d08db3c8] = 8
 handler table  [0x00007fd4d08db3c8,0x00007fd4d08db3f8] = 48
----------------------------------------------------------------------
Demo.square(I)I  [0x00007fd4d08db1c0, 0x00007fd4d08db2b8]  248 bytes
[Entry Point]
[Constants]
  # {method} {0x00007fd4b841f4b0} 'square' '(I)I' in 'Demo'
  # this:       rsi:rsi   = 'Demo'
  # parm0:      rdx     = int
  #             [sp+0x20]  (sp of caller)
[...]
[Stub Code]
  0x00007fd4d08db280: movabs $0x0,%rbx          ;   {no_reloc}
  0x00007fd4d08db28a: jmpq   0x00007fd4d08db28a  ;   {runtime_call}
  0x00007fd4d08db28f: movabs $0x0,%rbx          ;   {static_stub}
  0x00007fd4d08db299: jmpq   0x00007fd4d08db299  ;   {runtime_call}
[Exception Handler]
  0x00007fd4d08db29e: jmpq   0x00007fd4d08bb880  ;   {runtime_call ExceptionBlob}
[Deopt Handler Code]
  0x00007fd4d08db2a3: callq  0x00007fd4d08db2a8
  0x00007fd4d08db2a8: subq   $0x5,(%rsp)
  0x00007fd4d08db2ad: jmpq   0x00007fd4d08a01a0  ;   {runtime_call DeoptimizationBlob}
  0x00007fd4d08db2b2: hlt    
  0x00007fd4d08db2b3: hlt    
  0x00007fd4d08db2b4: hlt    
  0x00007fd4d08db2b5: hlt    
  0x00007fd4d08db2b6: hlt    
  0x00007fd4d08db2b7: hlt    
ImmutableOopMap{rbp=NarrowOop }pc offsets: 96
ImmutableOopMap{}pc offsets: 112
ImmutableOopMap{rbp=Oop }pc offsets: 148 Square(i) = 25
Time taken= 2567698
--------------------------------
6 iteration
Square(i) = 36
Time taken= 76752
[...]
--------------------------------
10 iteration
Square(i) = 100
Time taken= 52888

我只截取了输出中与 Demo.java 相关的部分。

现在再来看看 AOT 编译。它是在 JDK9 中引入的特性。AOT 是用于生成 .so 这样的库文件的静态编译器。用 AOT 可以将指定的类编译成 .so 库。这个库可以直接执行,而不用解释或 JIT 编译。如果 JVM 没有检测到 AOT 编译的代码,它会进行常规的解释和 JIT 编译。

使用 AOT 编译的命令如下:

$ jaotc --output=libDemo.so Demo.class

用下面的命令来查看共享库的符号表:

$ nm libDemo.so

要使用生成的 .so 库,使用 -XX:+UnlockExperimentalVMOptions-XX:AOTLibrary

$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so Demo
1 iteration
Square(i) = 1
Time taken= 7831139
--------------------------------
2 iteration
Square(i) = 4
Time taken= 36619
[...]
10 iteration
Square(i) = 100
Time taken= 42085

从输出上看,跟完全用解释的情况没有区别。为了确认 AOT 发挥了作用,使用 -XX:+PrintAOT

$ java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
         28        1         loaded        ./libDemo.so  aot library
         80        1         aot[ 1]   Demo.main([Ljava/lang/String;)V
         80        2         aot[ 1]   Demo.square(I)I
         80        3         aot[ 1]   Demo.<init>()V
1 iteration
Square(i) = 1
Time taken= 7252921
--------------------------------
2 iteration
Square(i) = 4
Time taken= 57443
[...]
10 iteration
Square(i) = 100
Time taken= 53586

要确认没有发生 JIT 编译,用如下命令:

$ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation \ -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation \ -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
         19        1         loaded        ./libDemo.so  aot library
         77        1         aot[ 1]   Demo.square(I)I
         77        2         aot[ 1]   Demo.main([Ljava/lang/String;)V
         77        3         aot[ 1]   Demo.<init>()V
         77        2         aot[ 1]   Demo.main([Ljava/lang/String;)V   made not entrant
[...]
4 iteration
Square(i) = 16
Time taken= 43366
[...]
10 iteration
Square(i) = 100
Time taken= 59554

需要特别注意的是,修改被 AOT 编译了的源代码后,一定要重新生成 .so 库文件。否则,过时的的 AOT 编译库文件不会起作用。例如,修改 square() 方法,使其计算立方值:

//Demo.java
public class Demo {

    public int square(int i) throws Exception {
        return(i*i*i);
    }

    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 10; i++) {
          System.out.println("" + Integer.valueOf(i)+" iteration");
          long start = System.nanoTime();
          int r= new Demo().square(i);
          System.out.println("Square(i) = " + r);
          long end = System.nanoTime();
          System.out.println("Time taken= " + (end-start));
          System.out.println("--------------------------------");
        }
    }
}

重新编译 Demo.java

$ java Demo.java

但不重新生成 libDemo.so。使用下面命令运行 Demo

$ java -XX:+UnlockExperimentalVMOptions -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:-TieredCompilation -XX:CompileThreshold=3 -XX:AOTLibrary=./libDemo.so -XX:+PrintAOT Demo
         20        1         loaded        ./libDemo.so  aot library
         74        1         n           java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native)   (static)
2 iteration
sqrt(i) = 8
Time taken= 43838
--------------------------------
3 iteration
        137   56        b            Demo::<init> (5 bytes)
        138   57        b            Demo::square (6 bytes)
sqrt(i) = 27
Time taken= 534649
--------------------------------
4 iteration
sqrt(i) = 64
Time taken= 51916
[...]
10 iteration
sqrt(i) = 1000
Time taken= 47132

可以看到,虽然旧版本的 libDemo.so 被加载了,但 JVM 检测出它已经过时了。每次生成 .class 文件时,都会在类文件中添加一个指纹,并在 AOT 库中保存该指纹。修改源代码后类指纹与旧的 AOT 库中的指纹不匹配了,所以没有执行 AOT 编译生成的原生机器码。从输出可以看出,现在实际上是 JIT 在起作用(注意 -XX:CompileThreshold 被设置为了 3)。

AOT 和 JIT 之间的权衡

如果你的目标是减少 JVM 的预热时间,请使用 AOT,这可以减少运行时负担。问题是 AOT 没有足够的数据来决定哪段代码需要预编译为原生代码。相比之下,JIT 在运行时起作用,却对预热时间有一定的影响。然而,它将有足够的分析数据来更高效地编译和反编译代码。

(题图:MJ/ed3e6e15-56c7-4c1d-aff1-84a225faeeeb)


via: https://opensource.com/article/22/8/interpret-compile-java

作者:Jayashree Huttanagoudar 选题:lkxed 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

一份让你深入体验最新 Linux 内核编译过程的实操指南。

出于各种原因,自行编译 Linux 内核可能引起你的兴趣。这些原因可能包括但不限于:

  • 测试一个比你目前的 Linux 发行版更新的内核版本
  • 采用一组不同的配置选项、驱动来构建内核
  • 学习者的好奇心 ?

此指南将一步步指导你如何亲自编译 Linux 内核,包括你该运行哪些命令,为什么运行这些命令以及这些命令的执行效果。本文篇幅较长,所以请做好准备!

? 诸如 Ubuntu 这样的发行版提供了更简单地安装主线 Linux 内核的方式。但本教程目标是从源码手动完成所有工作。此教程需要你付出时间、耐心以及丰富的 Linux 命令行使用经验。本文更注重亲身实践的体验。不管怎么说,我仍建议你在虚拟机或备用系统中尝试此冒险,而非在你的主系统上进行。

前置准备

在软件领域,构建任何事物都有两个基本要求:

  1. 源代码
  2. 构建依赖

因此,作为预备环节,我们需要下载 Linux 内核的源码压缩包,并安装一些能让我们成功构建 Linux 内核的依赖项。

Linux 版本导览

在任何时刻,Freax Linux 内核都有四种“版本”。

Linux 的这些 “版本”,按照开发流程的顺序是:

  1. linux-next 树: 所有准备合并到 Linux 代码库的代码首先被合并到 linux-next 树。它代表的是 Linux 内核最新也是“最不稳定”的状态。大多数 Linux 内核开发者和测试人员使用这个来提高代码质量,为 Linus Torvalds 的后续提取做准备。请谨慎使用!
  2. 发布候选版(RC) / 主线版: Linus 从 linux-next 树抽取代码并创建一个初始发布版本。这个初始发布版本的测试版称为 RC( 发布候选 Release Candidate )版本。一旦 RC 版本发布,Linus 只会接受对它的错误修复和性能退化相关的补丁。基础这些反馈,Linus 会每周发布一个 RC 内核,直到他对代码感到满意。RC 发行版本的标识是 -rc 后缀,后面跟一个数字。
  3. 稳定版: 当 Linus 觉得最新的 RC 版本已稳定时,他会发布最终的“公开”版本。稳定发布版将会维护几周时间。像 Arch Linux 和 Fedora Linux 这样的前沿 Linux 发行版会使用此类版本。我建议你在试用 linux-next 或任何 RC 版本之前,先试一试此版本。
  4. LTS 版本: 每年最后一个稳定版将会再维护 几年。这通常是一个较旧的版本,但它会 会积极地维护并提供安全修复。Debian 的稳定版本会使用 Linux 内核的 LTS 版版本。

若想了解更多此方面的知识,可参阅 官方文档

本文将以当前可用的最新稳定版为例,编写此文时的 Linux 内核版本是 6.5.5

系统准备

由于 Linux 内核使用 C 语言编写,编译 Linux 内核至少需要一个 C 编译器。你的计算机上可能还需要其他一些依赖项,现在是安装它们的时候了。

? 这个指南主要聚焦于使用 GNU C 编译器(GCC)来编译 Linux 内核。但在未来的文章中(可能会深入介绍 Rust 的支持),我可能会介绍使用 LLVM 的 Clang 编译器作为 GCC 的替代品。

不过,请注意,MSVC 并不适用。尽管如此,我仍期待有微软的员工为此发送修补程序集。我在瞎想啥?

对于 Arch Linux 以及其衍生版本的用户,安装命令如下:

sudo pacman -S base-devel bc coreutils cpio gettext initramfs kmod libelf ncurses pahole perl python rsync tar xz

对于 Debian 以及其衍生版本的用户,安装命令如下:

sudo apt install bc binutils bison dwarves flex gcc git gnupg2 gzip libelf-dev libncurses5-dev libssl-dev make openssl pahole perl-base rsync tar xz-utils

对于 Fedora 以及其衍生版本的用户,安装命令如下:

sudo dnf install binutils ncurses-devel \
    /usr/include/{libelf.h,openssl/pkcs7.h} \
    /usr/bin/{bc,bison,flex,gcc,git,gpg2,gzip,make,openssl,pahole,perl,rsync,tar,xz,zstd}

下载 Linux 内核源码

请访问 kernel.org,在页面中寻找第一个 稳定 Stable 版本。你不会找不到它,因为它是最显眼的黄色方框哦 ?

点击访问 kernel.org

通过点击黄色的方框,你就可以下载 Tar 文件。同时,也别忘了下载相匹配的 PGP 签名文件,稍后我们需要用到它来验证 Tar 文件。它的扩展名为 .tar.sign

校验 Tar 文件的完整性

你如何知道刚下载的 Tar 文件是否被损坏?对于个人来说,一个损坏的 Tar 文件只会浪费你的宝贵时间,如果你是在为一个组织工作,那么可能会危及到组织的安全(这时你可能还有更大的问题需要担忧,但我们并不想让所有人都产生创伤后应激障碍!)。

为了验证我们的 Tar 文件的完整性,我们需要先解压它。目前,它是使用 XZ 压缩算法压缩的。因此,我将使用 unxz 工具(其实就是 xz --decompress 的别名)来解压 .tar.xz 格式的压缩文件。

unxz --keep linux-*.tar.xz

解压完成后,我们需要获取 Linus Torvalds 和 Greg KH 使用的 GPG 公开密钥。这些密钥用于对 Tar 文件进行签名。

gpg2 --locate-keys [email protected] [email protected]

你应该可以得到一个与我在我的电脑上看到的类似的结果:

$ gpg2 --locate-keys [email protected] [email protected]
gpg: /home/pratham/.gnupg/trustdb.gpg: trustdb created
gpg: key 38DBBDC86092693E: public key "Greg Kroah-Hartman <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg: key 79BE3E4300411886: public key "Linus Torvalds <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
pub   rsa4096 2011-09-23 [SC]
      647F28654894E3BD457199BE38DBBDC86092693E
uid           [ unknown] Greg Kroah-Hartman <[email protected]>
sub   rsa4096 2011-09-23 [E]

pub   rsa2048 2011-09-20 [SC]
      ABAF11C65A2970B130ABE3C479BE3E4300411886
uid           [ unknown] Linus Torvalds <[email protected]>
sub   rsa2048 2011-09-20 [E]

在导入 Greg 和 Linus 的密钥后,我们可以使用 --verify 标志来验证 Tar 的完整性,操作如下:

gpg2 --verify linux-*.tar.sign

如果验证成功,你应该会看到如下的输出信息:

$ gpg2 --verify linux-*.tar.sign
gpg: assuming signed data in 'linux-6.5.5.tar'
gpg: Signature made Saturday 23 September 2023 02:46:13 PM IST
gpg:                using RSA key 647F28654894E3BD457199BE38DBBDC86092693E
gpg: Good signature from "Greg Kroah-Hartman <[email protected]>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 647F 2865 4894 E3BD 4571  99BE 38DB BDC8 6092 693E

务必查看是否存在 gpg: Good signature 的提示,然后再继续!

? 你可以忽略以下警告:WARNING: This key is not certified with a trusted signature! There is no indication that the signature belongs to the owner.

我们已根据 Linus 和 Greg 的邮件地址获取了公开密钥,并无需对此警告感到担忧。

解压 Tar 文件

如果你顺利的进行到这里,意味着你的 Tar 文件完整性检查已经成功完成。接下来,我们将从 Tar 文件中解压出 Linux 内核的源码。

The "TAR" xkcd comic: https://xkcd.com/1168/

这个步骤十分简单,只需对 Tar 文件执行 tar -xf 命令,如下:

tar -xf linux-*.tar

在这里,-x 选项表示解压,-f 选项则用来告诉 Tar 文件的文件名。

这个解压过程可能需要几分钟时间,你可以先放松,耐心等待一下。

配置 Linux 内核

Linux 内核的构建过程会查找 .config 文件。顾名思义,这是一个配置文件,用于指定 Linux 内核的所有可能的配置选项。这是必需的文件。

获取 Linux 内核的 .config 文件有两种方式:

  1. 使用你的 Linux 发行版的配置作为基础(推荐做法
  2. 使用默认的,通用的配置
? 也有第三种方法,也就是从零开始,手动配置每一个选项,但注意,这需要配置超过 12,000 个选项。并不推荐这种方式,因为手动配置所有选项将花费大量的时间,并且你还需要理解每个启用和禁用选项的含义。

使用发行版提供的配置

使用你的 Linux 发行版提供的配置是一个安全的选择。 如果你只是跟随这个指南测试一个不是你的发行版提供的新内核,那么这就是推荐的方式。

你的 Linux 发行版的 Linux 内核配置文件会在以下两个位置之一:

  • 大多数 Linux 发行版,如 Debian 和 Fedora 及其衍生版,将会把它存在 /boot/config-$(uname -r)
  • 一些 Linux 发行版,比如 Arch Linux 将它整合在了 Linux 内核中。所以,可以在 /proc/config.gz 找到。
? 如果两者都有,建议使用 /proc/config.gz。这是因为它在只读文件系统中,所以是未被篡改的。

进入含有已经解压出的 Tar 文件的目录。

cd linux-*/

接着,复制你的 Linux 发行版的配置文件:

### Debian 和 Fedora 及其衍生版:
$ cp /boot/config-"$(uname -r)" .config

### Arch Linux 及其衍生版:
$ zcat /proc/config.gz > .config
更新配置文件

一旦完成这些步骤,接下来就需要“更新”配置文件了。因为你的发行版提供的配置很可能比你正在构建的 Linux 内核版本要旧。

? 这同样适用于像 Arch Linux 和 Fedora 这样前沿的 Linux 发行版。 它们并不会因为有新版本可用就立刻发布更新。他们会进行一些质量控制工作,这必然会花费些时间。因此,即便是你的发行版提供的最新内核,相较于你在 kernel.org 上获取的版本也会滞后几个小版本。

要更新一个已有的 .config 文件,我们使用 make 命令搭配 olddefconfig 参数。简单解释一下,这个命令的意思是使用 旧的、默认的、配置

这将使用“旧的配置文件”(当前保存为 .config,这是你发行版配置的一份直接副本),并检查从上一版本以来 Linux 代码库中新加的任何配置选项。如果找到任何新的、未配置 的选项,该选项的默认配置值会被使用,并会对 .config 文件进行更新。

原来的 .config 文件将被重命名为 .config.old 进行备份,并将新的更改写入至 .config 文件。

make olddefconfig

以下是我机器上的输出:

$ file .config
.config: Linux make config build file, ASCII text

$ make olddefconfig
    HOSTCC  scripts/basic/fixdep
    HOSTCC  scripts/kconfig/conf.o
    HOSTCC  scripts/kconfig/confdata.o
    HOSTCC  scripts/kconfig/expr.o
    LEX     scripts/kconfig/lexer.lex.c
    YACC    scripts/kconfig/parser.tab.[ch]
    HOSTCC  scripts/kconfig/lexer.lex.o
    HOSTCC  scripts/kconfig/menu.o
    HOSTCC  scripts/kconfig/parser.tab.o
    HOSTCC  scripts/kconfig/preprocess.o
    HOSTCC  scripts/kconfig/symbol.o
    HOSTCC  scripts/kconfig/util.o
    HOSTLD  scripts/kconfig/conf
.config:8593:warning: symbol value 'm' invalid for USB_FOTG210_HCD
.config:8859:warning: symbol value 'm' invalid for USB_FOTG210_UDC
#
# configuration written to .config
#
针对 Debian 及其衍生版用户

Debian 及其衍生版为内核模块使用一个签名证书。默认情况下,你的计算机并不包含这个证书。

我推荐关闭启用模块签名的选项。具体如下所示:

./scripts/config --file .config --set-str SYSTEM_TRUSTED_KEYS ''
./scripts/config --file .config --set-str SYSTEM_REVOCATION_KEYS ''

如果你不这么做,在后面你进行 Linux 内核构建时,可能会导致构建失败。要注意这点。

使用自定义配置

如果你出于学习内核开发的目的学习如何构建 Linux 内核,那你应该这样做。

? 请注意,偏离你的 Linux 发行版的配置可能无法在实体硬件上“正常”工作。问题可能是特定硬件无法工作、Linux 内核无法启动等。

因此,我们只建议在虚拟机中使用。

你可以通过查看 make help 的输出 来查看 所有 可用的选项,但我们主要关注三个 make 目标:

  • defconfig: 默认配置。
  • allmodconfig: 根据当前系统状态,尽可能地把项目构建为可加载模块(而非内建)。
  • tinyconfig: 极简的 Linux 内核。

由于 tinyconfig 目标只会构建少数项目,构建时间将会缩短。我个人选择它的原因主要有:

  1. 检查我在代码/工具链中做的修改是否正确,以及代码是否可以编译。
  2. 在虚拟机中只进行少数选项的测试。

? 在为 ARM 或 RISC-V 机器构建 Linux 内核时,你可能需要 DTB(设备树的二进制文件)。使用 tinyconfig 目标将不会启用构建 DTB 的选项,你的内核很可能无法启动。

当然,你可以用 QEMU 在没有任何 DTB 的情况下启动 Linux 内核。但这篇文章并不会聚焦在此。或许你可以通过评论,让我在之后的时间里覆盖这个话题 ?

除非你确切地知道自己在做什么,否则你应当使用 defconfig 目标。 以下是我在我的电脑上运行的效果:

$ make defconfig
    HOSTCC  scripts/basic/fixdep
    HOSTCC  scripts/kconfig/conf.o
    HOSTCC  scripts/kconfig/confdata.o
    HOSTCC  scripts/kconfig/expr.o
    LEX     scripts/kconfig/lexer.lex.c
    YACC    scripts/kconfig/parser.tab.[ch]
    HOSTCC  scripts/kconfig/lexer.lex.o
    HOSTCC  scripts/kconfig/menu.o
    HOSTCC  scripts/kconfig/parser.tab.o
    HOSTCC  scripts/kconfig/preprocess.o
    HOSTCC  scripts/kconfig/symbol.o
    HOSTCC  scripts/kconfig/util.o
    HOSTLD  scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#

修改配置

无论你是使用 Linux 发行版的配置并更新它,还是使用 defconfig 目标创建新的 .config 文件,你都可能希望熟悉如何修改这个配置文件。最可靠的修改方式是使用 menuconfignconfig 目标。

这两个目标的功能是相同的,只不过提供给你的界面有所不同。这是这两者间唯一的区别。我个人更偏向于使用 menuconfig 目标,但近来我发现 nconfig 在搜索选项时似乎更具直观性,所以我逐渐转向使用它。

首先,带着 menuconfig 目标运行 make 命令:

$ make menuconfig
    HOSTCC  scripts/kconfig/mconf.o
    HOSTCC  scripts/kconfig/lxdialog/checklist.o
    HOSTCC  scripts/kconfig/lxdialog/inputbox.o
    HOSTCC  scripts/kconfig/lxdialog/menubox.o
    HOSTCC  scripts/kconfig/lxdialog/textbox.o
    HOSTCC  scripts/kconfig/lxdialog/util.o
    HOSTCC  scripts/kconfig/lxdialog/yesno.o
    HOSTLD  scripts/kconfig/mconf

在此界面,你可以根据各选项的类型来进行切换操作。

有两类可切换选项:

  • 布尔状态选项:这类选项只能关闭([ ])或作为内建组件开启([*])。
  • 三态选项:这类选项可以关闭(< >)、内建(<*>),或作为可加载模块(<M>)进行构建。

想要了解更多关于某个选项的信息,使用上/下箭头键导航至该选项,然后按 <TAB> 键,直至底部的 < Help > 选项被选中,然后按回车键进行选择。此时就会显示关于该配置选项的帮助信息。

在修改选项时请务必谨慎。

当你满意配置后,按 <TAB> 键直到底部的 < Save > 选项被选中。然后按回车键进行选择。然后再次按回车键(记住,此时不要更改文件名),就能将更新后的配置保存到 .config 文件中。

构建 Linux 内核

构建 Linux 内核实际上十分简单。然而,在开始构建之前,让我们为自定义内核构建添加一个标签。我将使用字符串 -pratham 作为标签,并利用 LOCALVERSION 变量来实施。你可以使用以下命令实现配置:

./scripts/config --file .config --set-str LOCALVERSION "-pratham"

这一命令将 .config 文件中的 CONFIG_LOCALVERSION 配置选项设为我在结尾指定的字符串,即 -pratham。当然,你也不必非得使用我所用的名字哦 ?

LOCALVERSION 选项可用于设置一个“本地”版本,它会被附加到通常的 x.y.z 版本方案之后,并在你运行 uname -r 命令时一并显示。

由于我正在构建的是 6.5.5 版本内核,而 LOCALVERSION 字符串被设为 -pratham,因此,对我来说,最后的版本名将会是 6.5.5-pratham。这么做的目的是确保我所构建的自定义内核不会与发行版所提供的内核产生冲突。

接下来,我们来真正地构建内核。可以用以下的命令完成此步骤:

make -j$(nproc) 2>&1 | tee log

这对大部分(99%)用户来说已经足够了。

其中的 -j 选项用于指定并行编译任务的数量。而 nproc 命令用于返回可用处理单位(包括线程)的数量。因此,-j$(nproc) 其实意味着“使用我拥有的 CPU 线程数相同数量的并行编译任务”。

2>&1 会将 STDOUT 和 STDIN 重定向到相同的文件描述符,并通过管道传输给 tee 命令,这会将输出存储在一个名为 log 的文件,并且在控制台打印出完全相同的文本。如果你在构建时遇到错误,并希望回顾日志来检查出了什么问题,这将会十分有用。遇到那种情况,你只需要简单执行 grep Error log 命令就能找到线索。

自定义 make 目标

在 Linux 内核的源文件夹中,make 命令有一些自定义的目标可供执行各种操作。这些主要作为开发者的参考。如果你的唯一目标是安装一个比你当前发行版更新的 Linux 内核,那么你完全可以跳过这部分内容 ?

构建目标

作为一名开发者,你可能只想构建 Linux 内核,或者只想构建模块,或者只想构建设备树二进制(DTB)。在这种情况下,你可以指定一个构建目标,然后 make 命令只会构建指定的项目,而不会构建其他的。

以下是一些构建目标:

  • vmlinux:纯粹的 Linux 内核。
  • modules:可加载模块。
  • dtbs:设备树二进制文件(主要用于 ARM 和 RISC-V 架构)。
  • all:构建所有被标记了星号 * 的项目(从 make help 的输出中可以查看)。

通常情况下,你并不需要指定构建目标,因为它们都已经在构建列表中。所列出的目标是在你只想要测试某一个构建目标,而不是其他目标时的情况。

依据你的 计算机架构,构建完成的 Linux 内核镜像(存放在 /boot 目录)的名称会有所不同。

对于 x86_64,Linux 内核的默认镜像名称是 bzImage。因此,如果你只需要构建引导所需的 Linux 内核,你可以像下面这样设定 bzImage 为目标:

### 对于 x86_64
$ make bzImage

“那么如何在我的架构上找到用来调用 make 的目标名称呢?”

有两种方法。要么你可以执行 make help 之后查找在 Architecture specific targets 下,第一个前面带有星号 * 的选项。

或者,如果你希望自动完成,你可以利用 image_name 目标得到镜像的完全路径(相对路径),选择性地添加 -s 标志来获得有用的输出。

以下是我拥有的三台电脑的输出,一台是 x86_64,另一台是 AArch64,还有一台是 riscv

### x86_64
$ make -s image_name
arch/x86/boot/bzImage

### AArch64
$ make -s image_name
arch/arm64/boot/Image.gz

### RISC-V
$ make -s image_name
arch/riscv/boot/Image.gz

现在,要只构建 Linux 内核镜像,你可以这样进行:

make $(make -s image_name | awk -F '/' '{print $4}')
清理目标

如果你需要清理构建产生的文件,你可以用以下的目标来实现你的需求:

  • clean:除了 .config 文件外,删除几乎所有其他内容。
  • mrproper:执行了 make clean 的所有操作外,还会删除 .config 文件。
  • distclean:除了执行 make mrproper 的所有操作外,还会清理任何补丁文件。

安装

一旦成功编译了 Linux 内核,接下来就是启动安装一些东西的时候了。“一些 东西?” 没错,我们至少构建了两种不同的东西,如果你使用的是 ARM 或 RISC-V 架构,那就有三种。我会在以下内容中详细解释。

? 虽然我将告诉你不同的安装方式,尤其是关于如何改变默认安装路径的方法,但如果你不确定自己在做什么,那么我不建议你这么做! 请慎重考虑,如果你决定走自定义的路线,那你需要自己负责后果。默认设置之所以存在,是因为它们有其特殊的原因 ?

安装内核模块

Linux 内核有部分在系统启动时并非必需的。这些部分被构建为可加载模块,即在需要时才进行加载和卸载。

所以,首先需要安装这些模块。这可以通过 modules_install 目标完成。必须使用 sudo,因为模块会被安装在 /lib/modules/<kernel_release>-<localversion> 这个需要 root 权限的路径下。

这个过程不仅会安装内核模块,还会对其进行签名,所以可能需要一些时间。好消息是你可以通过之前提到的 -j$(nproc) 选项来并行执行安装任务,这样会快一些。?

sudo make modules_install -j$(nproc)

给开发者的提示: 你可以通过设定 INSTALL_MOD_PATH 变量来指定一个不同的路径存放 Linux 模块,而不用默认的 /lib/modules/<kernel_release>-<localversion>,具体如下:

   sudo make modules_install INSTALL_MOD_PATH=<path>
另一个给开发者的提示: 你可以使用 INSTALL_MOD_STRIP 变量来决定是否需要剥离模块的调试符号。如果未设定该变量,调试符号不会被剥离。当设为 1 时,符号信息将会被使用 --strip-debug 选项剥离,随后该选项会传递给 strip(或者在使用 Clang 的时候传递给 llvm-strip)工具。

(可选)安装 Linux 内核头文件

如果你打算使用这个内核来支持树外模块,比如 ZFS 或英伟达 DKMS,或者打算尝试自行编写模块,你可能会需要 Linux 内核提供的头文件。

可以通过以下方式使用 headers_install 目标来安装 Linux 内核头文件:

sudo make headers_install

应使用 sudo 命令,因为这些头文件会被安装到 /usr 目录。同时还会在 /usr 目录内创建子目录 include/linux,然后将头文件安装到 /usr/include/linux 内。

给开发者的提示: 通过设定 INSTALL_HDR_PATH 变量,你可以修改 Linux 内核头文件的安装路径。

安装 DTB(只针对 ARM 和 RISC-V)

如果你使用的是 x86\_64 架构,那么你可以跳过此步骤!

如果你针对 ARM 或者 RISC-V 构建了内核,那么在运行 make 的过程中,设备树的二进制文件可能已经被编译出来了。你可以通过在 arch/<machine_architecture>/boot/dts 目录查找 .dtb 文件来确认这一点。

这里提供了一个快速检查的技巧:

### 对于 AArch32
$ find arch/arm/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM32 were built"

### 对于 AArch64
$ find arch/arm64/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM64 were built"

### 对于 RISC-V
$ find arch/riscv/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for RISC-V were built"

如果你看到出现 DTBs for <arch> were built 的消息,那么你可以开始安装 DTB。这可以通过 dtbs_install 目标来实现。

需要使用 sudo,因为它们会被安装在 /boot/dtb-<kernel_release>-<localversion> 中,而这个目录是由 root 所拥有的。

sudo make dtbs_install
给开发者的提示: 就像安装模块一样,你可以使用 INSTALL_DTBS_PATH 变量指定一个自定义的路径来安装设备树二进制文件。

安装 Linux 内核

最后,我们来安装 Linux 内核本身!这可以通过 install 目标来完成,就像这样:

sudo make install

在这里必须使用 sudo,因为 Linux 内核将被安装在 /boot 目录,而这个目录不允许普通用户写入。

? 一般来讲,install 目标也会更新引导加载程序,但是如果它没有成功,那可能是不支持你使用的引导加载程序。如果你没有使用 GRUB 作为你的引导加载程序,请一定要阅读你引导加载程序的使用手册 ?
给开发者的提示: 并不奇怪,INSTALL_PATH 变量被用来设定 Linux 内核的安装位置,而非默认的 /boot 目录。

针对 Arch Linux 用户的说明

如果你尝试执行了 make install 命令,可能已经注意到产生了错误。错误如下:

$ sudo make install
    INSTALL /boot
Cannot find LILO.

要在 Arch Linux 上实际完成 Linux 内核的安装,我们需要手动复制 Linux 内核镜像文件。别担心,如果你使用的是 Arch Linux,手动操作应该是家常便饭了。( ͡° ͜ʖ ͡°)

可以使用以下命令完成这个步骤:

sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-<kernel_release>-<localversion>

因为我编译的是 6.5.5 版本的内核,所以我将会执行下面这条命令,你可以根据你的实际情况进行适当调整:

sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-6.5.5-pratham

虽然不是必须的,但最好复制一份名为 System.map 的文件。既然你已经在操作了,一并也复制了 .config 文件吧 ?

sudo cp -vf System.map /boot/System.map-<kernel_release>-<localversion>
sudo cp -vf .config /boot/config-<kernel_release>-<localversion>
生成初始 RAM 磁盘

当你安装 Arch Linux 时,可能已经了解过 mkinitcpio 这个工具。现在,我们将使用它来创建初始的 RAM 磁盘。

首先,我们需要创建一个预设文件。向 /etc/mkinitcpio.d/linux-<localversion>.preset 文件中添加以下内容,根据实际需要来替换 <kernel_release><localversion>

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-<kernel_release>-<localversion>"

PRESETS=('default' 'fallback')

default_image="/boot/initramfs-<kernel_release>-<localversion>.img"
fallback_options="-S autodetect"

配置完成后,执行下面的命令来生成初始 RAM 磁盘:

sudo mkinitcpio -p linux-<localversion>

我自己的电脑上得到的输出如下,你的结果应该会类似!

$ sudo mkinitcpio -p linux-pratham
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'default'
==> Using configuration file: '/etc/mkinitcpio.conf'
    -> -k /boot/vmlinuz-6.5.5-pratham -c /etc/mkinitcpio.conf -g /boot/initramfs-6.5.5-pratham.img
==> Starting build: '6.5.5-pratham'
    -> Running build hook: [base]
    -> Running build hook: [udev]
    -> Running build hook: [autodetect]
    -> Running build hook: [modconf]
    -> Running build hook: [kms]
    -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
    -> Running build hook: [keymap]
    -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
    -> Running build hook: [block]
    -> Running build hook: [filesystems]
    -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-6.5.5-pratham.img'
==> Image generation successful
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'fallback'
==> Using configuration file: '/etc/mkinitcpio.conf'
==> WARNING: No image or UKI specified. Skipping image 'fallback'

初始 RAM 磁盘已成功生成,现在我们可以进入下一步,更新引导加载器!

更新 GRUB

一旦所有必要的文件已成功复制到其对应的位置,接下来,我们将进行 GRUB 的更新。

使用以下命令对 GRUB 引导加载器进行更新:

sudo grub-mkconfig -o /boot/grub/grub.cfg
? 如果你使用的引导加载器不是 GRUB,请参看 Arch Wiki 中相关的引导加载器文档。

注意,更新 GRUB 并不会直接使新的内核版本设为默认启动选项。在引导时,请在启动菜单中手动选择新的内核版本。

你可以通过选择 Advanced options for Arch Linux 菜单,并在随后的菜单中选择 Arch Linux, with Linux <kernel_release>-<localversion> 来启用新版的 Linux 内核。

重启电脑

恭喜你!你已经完成了获取 Linux 内核源代码、进行配置、构建以及安装等所有步骤。现在只需要通过重启电脑并进入新构建和安装的 Linux 内核,就可以开始享受你的努力成果了。

启动时,请确保从引导加载器中选择正确的 Linux 内核版本。系统启动后,运行 uname -r 命令来确认你正在使用预期的 Linux 内核。

以下是我自己的电脑输出的内容:

$ uname -r
6.5.5-pratham

是时候开始庆祝了! ?

卸载操作

? 提示:在删除当前正在使用的内核版本之前,你应该首先切换至较旧的内核版本。

可能你的 Linux 发行版所使用的 Linux 内核版本就是你手动编译的版本,或者你自行编译了新的内核并注意到应卸载旧的内核以节省空间,于是你开始想如何才能卸载。当然,虽然我们无法简单地运行 make uninstall 命令,但这并不代表没有其他的方法!

我们清楚各个文件的安装位置,因此删除它们相对简单。

### 删除内核模块
$ rm -rf /lib/modules/<kernel_release>-<localversion>

### 删除设备树二进制文件
$ rm -rf /boot/dtb-<kernel_release>-<localversion>

### 删除 Linux 内核本身
$ rm -vf /boot/{config,System,vmlinuz}-<kernel_release>-<localversion>

总结

这个过程不是一次简单的旅程,是吧?但是现在,我们终于抵达了终点。我们一起学习了手动编译 Linux 内核的全过程,包括安装依赖、获取和验证源码、解压源码、配置 Linux 内核、构建内核以及安装内核。

如果你喜欢这个详细的步骤指南,请给我留言反馈。如果在操作过程中遇到问题,也欢迎提出,让我知道!

(题图:MJ/853481c5-87e3-42aa-8ace-e9ddfa232f75)


via: https://itsfoss.com/compile-linux-kernel/

作者:Pratham Patel 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

用这个方便的捕鼠器比喻来理解编译代码。

源代码必须要经过编译才能够运行程序,而对于开源软件,每个人都可以获取源代码。无论你是自己编写了代码,想要编译和运行它,还是下载了某人的项目来尝试它,了解如何通过 编译器 处理源代码,以及编译器如何处理这些代码,这都很有用。

创建一个更好的捕鼠器

一般情况我们不会将一个捕鼠器比作电脑,但不管你信不信,它确实与你正在使用的设备(手机或电脑)的 CPU 有一些相似之处。经典的捕鼠器(我说的不是 ?)有两种状态:打开或者释放。你可以认为 打开 是将捕鼠器设置好准备捕获老鼠,以及 释放 是捕鼠器被老鼠触发。某种意义上来说,捕鼠器就像是一台有鼠标的电脑。你可以想象一下这个代码,用一种虚构的语言来描述这个过程:

if mousetrap == 0 then
  There's a mouse!
else
  There's no mouse yet.
end

换句话说,你可以基于捕鼠器的状态发现是否有老鼠(数据)。当然,捕鼠器不是万无一失的,有可能有一只老鼠在捕鼠器旁边,由于老鼠还没有触发捕鼠器,所以它的状态还是 打开 的。因此该程序可以进行改进,这都是非常典型的。

开关

总的来说,捕鼠器就是一个开关。你会在家里使用开关打开灯。可以从开关中获得许多信息。比如,人们会从你家灯的状态了解到你是否在家。

你可以根据邻居家灯的状态来改变行为。如果邻居家所有的灯都熄灭了,那么请关掉你大声的音乐,因为人们可能已经上床睡觉了。

CPU 也使用这样的逻辑,只不过乘以几个数量级,缩小到了微观级别。当 CPU 在特定寄存器上接收到电信号时,可以触发其他一些寄存器,然后触发另一个,以此类推。如果这些寄存器有特定的意义,那么就可以通信。也许激活同一主板上某处的芯片,或者使 LED 亮起,或者改变屏幕上的像素颜色。

种瓜得瓜,种豆得豆。如果你真的想在多个位置而不是仅限于一处发现老鼠,但是你只有一个捕鼠器,那你应该开发一个应用才行。使用网络摄像头和一些基本的图像识别软件,你可以建立空厨房的模型,然后扫描变化。当老鼠进入厨房,在原先没有老鼠的图像上会有像素的变化。记录下这些数据,如果有无人机可以追踪老鼠并捕获会更好,这样就可以将老鼠赶出厨房了。这时,你通过打开和关闭信号的魔法,创造了一个更好的捕鼠器。

编译器

代码编译器将人们可阅读的代码转换成 CPU 可以理解的机器语言。这是非常复杂的过程,因为 CPU 非常复杂(甚至比捕鼠器更加复杂),同时因为该过程比严格“需要”的更加灵活。并不是所有的编译器都很灵活。有一些编译器只有一个目标,它们只会处理特定格式的代码文件,处理过程也因此而简单明了。

幸运的是,现代的通用编译器并不简单。它们允许你编写不同语言的代码,也允许你用不同的方式链接库文件,并且可以生成运行在不同架构上的文件。GNU 编译器集合(GCC)的 gcc 编译器 --help 会输出超过 50 行的选项,LLVM 的 clang 编译器的 --help 输出超过 1000 行。GCC 指导手册的字数超过 10 万。

当你在编译代码时会有很多选项。

当然,大多数人并不需要知道所有的选项。我从未读过 GCC 的手册页,因为它们是针对 Objective-C、Fortran 以及我从未听说过的芯片架构的。不过我重视它将代码编译为不同的架构 —— 64 位或者 32 位 —— 的能力,以及在其他行业已经落后的计算机上运行开源软件的能力。

编译生命周期

同样重要的是,理解编译代码的不同阶段。这是一个简单的 C 语言程序的生命周期:

  1. 带有宏定义的 C 源代码 .c 文件,用 cpp 预处理为 .i 文件。
  2. 扩展了宏定义的 C 源代码 .i 文件,会被 gcc 转译成 .s 文件。
  3. 以汇编语言写的文本文件 .s 文件被汇编为目标 .o 文件。
  4. 带有 CPU 指令的二进制目标代码,以及其他目标文件和库 *.o 文件,以内存区域无关的偏移量,使用 ld 链接以生成可执行文件。
  5. 最终的二进制文件要么包含所有需要的目标,要么设置以动态链接库 *.so 文件加载。

你可以试试这个简单示例(可能需要对库路径做一些调整):

$ cat << EOF >> hello.c
 #include
 int main(void)
 { printf("hello world\n");
   return 0; }
   EOF
$ cpp hello.c > hello.i
$ gcc -S hello.i
$ as -o hello.o hello.s
$ ld -static -o hello \
  -L/usr/lib64/gcc/x86_64-slackware-linux/5.5.0/ \
  /usr/lib64/crt1.o /usr/lib64/crti.o hello.o \
  /usr/lib64/crtn.o  --start-group -lc -lgcc \
  -lgcc_eh --end-group
$ ./hello
hello world

可获得的知识

计算机已经变得非常强大,并且用户友好。请不要走向这两种可能的极端中的任何一种:计算机不像捕鼠器和电灯开关那么简单,但它们也不是无法理解的。你可以了解编译代码、如何链接以及针对不同架构进行编译。一旦你知道了,你就可以更好地调试代码。你可以理解你下载的代码,甚至可以修复其中的一两个错误。同时从理论上来讲,你可以建造一个更好的捕鼠器,或者用捕鼠器造一个 CPU。由你决定。


via: https://opensource.com/article/22/10/compiling-code

作者:Alan Smithee 选题:lkxed 译者:Donkey-Hao 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习如何用静态链接库将多个 C 目标文件结合到一个单个的可执行文件之中。

使用 C 编写的应用程序时,通常有多个源码文件,但最终你需要编译成单个的可执行文件。

你可以通过两种方式来完成这项工作:通过创建一个 静态 static 库 或 一个 动态 dynamic 库(也被称为 共享 shared 库)。从创建和链接的方式来看,它们是两种不同类型的库。选择使用哪种方式取决于你的的具体场景。

上一篇文章 中,我演示了如何创建一个动态链接的可执行文件,这是一种更通用的方法。在这篇文章中,我将说明如何创建一个静态链接的可执行文件。

使用静态库链接器

链接器 linker 是一个命令,它将一个程序的多个部分结合在一起,并为它们重新组织内存分配。

链接器的功能包括:

  • 整合一个程序的所有的部分
  • 计算出一个新的内存组织结构,以便所有的部分组合在一起
  • 恢复内存地址,以便程序可以在新的内存组织结构下运行
  • 解析符号引用

链接器通过这些功能,创建了一个名称为可执行文件的一个可运行程序。

静态库是通过复制一个程序中的所有依赖库模块到最终的可执行镜像来创建的。链接器将链接静态库作为编译过程的最后一步。可执行文件是通过解析外部引用、将库例程与程序代码结合在一起来创建的。

创建目标文件

这里是一个静态库的示例以及其链接过程。首先,创建带有这些函数识别标志的头文件 mymath.h :

int add(int a, int b);
int sub(int a, int b);
int mult(int a, int b);
int divi(int a, int b);

使用这些函数定义来创建 add.csub.cmult.cdivi.c 文件。我将把所有的代码都放置到一个代码块中,请将其分为四个文件,如注释所示:

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

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

//mult.c
int mult(int a, int b){
return (a*b);
}

//divi.c
int divi(int a, int b){
return (a/b);
}

现在,使用 GCC 来生成目标文件 add.osub.omult.odivi.o

(LCTT 校注:关于“ 目标文件 object file ”,有时候也被称作“对象文件”,对此,存在一些译法混乱情形,称之为“目标文件”的译法比较流行,本文采用此译法。)

$ gcc -c add.c sub.c mult.c divi.c

-c 选项跳过链接步骤,而只创建目标文件。

创建一个名称为 libmymath.a 的静态库,接下来,移除目标文件,因为它们不再被需要。(注意,使用一个 trash 命令比使用一个 rm 命令更安全。)

$ ar rs libmymath.a add.o sub.o mult.o divi.o
$ trash *.o
$ ls
add.c  divi.c  libmymath.a  mult.c  mymath.h  sub.c

现在,你已经创建了一个名称为 libmymath 的简单数学示例库,你可以在 C 代码中使用它。当然,也有非常复杂的 C 库,这就是他们这些开发者来生成最终产品的工艺流程,你和我可以安装这些库并在 C 代码中使用。

接下来,在一些自定义代码中使用你的数学库,然后链接它。

创建一个静态链接的应用程序

假设你已经为数学运算编写了一个命令。创建一个名称为 mathDemo.c 的文件,并将这些代码复制粘贴至其中:

#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  int x, y;
  printf("Enter two numbers\n");
  scanf("%d%d",&x,&y);
 
  printf("\n%d + %d = %d", x, y, add(x, y));
  printf("\n%d - %d = %d", x, y, sub(x, y));
  printf("\n%d * %d = %d", x, y, mult(x, y));

  if(y==0){
    printf("\nDenominator is zero so can't perform division\n");
      exit(0);
  }else{
      printf("\n%d / %d = %d\n", x, y, divi(x, y));
      return 0;
  }
}

注意:第一行是一个 include 语句,通过名称来引用你自己的 libmymath 库。

针对 mathDemo.c 创建一个名称为 mathDemo.o 的对象文件:

$ gcc -I . -c mathDemo.c

-I 选项告诉 GCC 搜索在其后列出的头文件。在这个实例中,你通过单个点(.)来指定当前目录。

链接 mathDemo.olibmymath.a 来生成最终的可执行文件。这里有两种方法来向 GCC 告知这一点。

你可以指向文件:

$ gcc -static -o mathDemo mathDemo.o libmymath.a

或者,你可以具体指定库的路径及名称:

$ gcc -static -o mathDemo -L . mathDemo.o -lmymath

在后面的那个示例中,-lmymath 选项告诉链接器来链接对象文件 mathDemo.o 和对象文件 libmymath.a 来生成最终的可执行文件。-L 选项指示链接器在下面的参数中查找库(类似于你使用 -I 所做的工作)。

分析结果

使用 file 命令来验证它是静态链接的:

$ file mathDemo
mathDemo: ELF 64-bit LSB executable, x86-64...
statically linked, with debug_info, not stripped

使用 ldd 命令,你将会看到该可执行文件不是动态链接的:

$ ldd ./mathDemo
        not a dynamic executable

你也可以查看 mathDemo 可执行文件的大小:

$ du -h ./mathDemo
932K    ./mathDemo

在我 前一篇文章 的示例中,动态链接的可执行文件只占有 24K 大小。

运行该命令来看看它的工作内容:

$ ./mathDemo
Enter two numbers
10
5

10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2

看起来令人满意!

何时使用静态链接

动态链接可执行文件通常优于静态链接可执行文件,因为动态链接会保持应用程序的组件模块化。假如一个库接收到一次关键安全更新,那么它可以很容易地修补,因为它存在于应用程序的外部。

当你使用静态链接时,库的代码会“隐藏”在你创建的可执行文件之中,意味着在库每次更新时(相信我,你会有更好的东西),仅有的一种修补方法是重新编译和发布一个新的可执行文件。

不过,如果一个库的代码,要么存在于它正在使用的具有相同代码的可执行文件中,要么存在于不会接收到任何更新的专用嵌入式设备中,那么静态连接将是一种可接受的选项。


via: https://opensource.com/article/22/6/static-linking-linux

作者:Jayashree Huttanagoudar 选题:lkxed 译者:robsean 校对:turbokernel

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习如何用动态链接库将多个 C 目标文件结合到一个单个的可执行文件之中。

当使用 C 编程语言编写一个应用程序时,你的代码通常有多个源文件代码。

最终,这些文件必须被编译到一个单个的可执行文件之中。你可以通过创建静态或动态库(后者也被称为 共享 shared 库)来实现这一点。这两种类型的库在创建和链接的方式上有所不同。两者都有缺点和优点,这取决于你的使用情况。

动态链接是最常见的方法,尤其是在 Linux 系统上。动态链接会保持库模块化,因此,很多应用程序可以共享一个库。应用程序的模块化也允许单独更新其依赖的共享库。

在这篇文章中,我将演示动态链接是如何工作的。在后期的文章中,我将演示静态链接。

链接器

链接器 linker 是一个命令,它将一个程序的数个部分结合在一起,并为它们重新组织内存分配。

链接器的功能包括:

  • 整合一个程序的所有的部分
  • 计算出一个新的内存组织结构,以便所有的部分组合在一起
  • 恢复内存地址,以便程序可以在新的内存组织结构下运行
  • 解析符号引用

链接器通过这些功能,创建了一个名为 可执行文件 executable 的可以运行的程序。在你创建一个动态链接的可执行文件前,你需要一些用来链接的库,和一个用来编译的应用程序。准备好你 最喜欢的文本编辑器 并继续。

创建目标文件

首先,创建带有这些函数签名的头文件 mymath.h

int add(int a, int b);
int sub(int a, int b);
int mult(int a, int b);
int divi(int a, int b);

使用这些函数定义来创建 add.csub.cmult.cdivi.c 文件。我将把所有的代码都放置到一个代码块中,请将其分为四个文件,如注释所示:

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

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

//mult.c
int mult(int a, int b){
return (a*b);
}

//divi.c
int divi(int a, int b){
return (a/b);
}

现在,使用 GCC 来创建目标文件 add.osub.omult.odivi.o

(LCTT 校注:关于“ 目标文件 object file ”,有时候也被称作“对象文件”,对此,存在一些译法混乱情形,称之为“目标文件”的译法比较流行,本文采用此译法。)

$ gcc -c add.c sub.c mult.c divi.c

-c 选项跳过链接步骤,并且只创建目标文件。

创建一个共享的目标文件

在最终的可执行文件的执行过程中将链接动态库。在最终的可执行文件中仅放置动态库的名称。实际上的链接过程发生在运行时,在此期间,可执行文件和库都被放置到了主内存中。

除了可共享外,动态库的另外一个优点是它减少了最终的可执行文件的大小。在一个应用程序最终的可执行文件生成时,其使用的库只包括该库的名称,而不是该库的一个多余的副本。

你可以从你现有的示例代码中创建动态库:

$ gcc -Wall -fPIC -c add.c sub.c mult.c divi.c

选项 -fPIC 告诉 GCC 来生成 位置无关的代码 position-independent code (PIC)。-Wall 选项不是必需的,并且与代码的编译方式是无关的。不过,它却是一个有价值的选项,因为它会启用编译器警告,这在排除故障时是很有帮助的。

使用 GCC ,创建共享库 libmymath.so

$ gcc -shared -o libmymath.so add.o sub.o mult.o divi.o

现在,你已经创建了一个简单的示例数学库 libmymath.so ,你可以在 C 代码中使用它。当然,也有非常复杂的 C 库,这就是他们这些开发者来生成最终产品的工艺流程,你和我可以安装这些库并在 C 代码中使用。

接下来,你可以在一些自定义代码中使用你的新数学库,然后链接它。

创建一个动态链接的可执行文件

假设你已经为数学运算编写了一个命令。创建一个名称为 mathDemo.c 的文件,并将这些代码复制粘贴至其中:

#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  int x, y;
  printf("Enter two numbers\n");
  scanf("%d%d",&x,&y);
 
  printf("\n%d + %d = %d", x, y, add(x, y));
  printf("\n%d - %d = %d", x, y, sub(x, y));
  printf("\n%d * %d = %d", x, y, mult(x, y));

  if(y==0){
    printf("\nDenominator is zero so can't perform division\n");
      exit(0);
  }else{
      printf("\n%d / %d = %d\n", x, y, divi(x, y));
      return 0;
  }
}

注意:第一行是一个 include 语句,通过名称来引用你自己的 libmymath 库。要使用一个共享库,你必须已经安装了它,如果你没有安装你将要使用的库,那么当你的可执行文件在运行并搜索其包含的库时,将找不到该共享库。如果你需要在不安装库到已知目录的情况下编译代码,这里有 一些方法可以覆盖默认设置。不过,对于一般使用来说,我们希望库存在于已知的位置,因此,这就是我在这里演示的东西。

复制文件 libmymath.so 到一个标准的系统目录,例如:/usr/lib64, 然后运行 ldconfigldconfig 命令创建所需的链接,并缓存到标准库目录中发现的最新共享库。

$ sudo cp libmymath.so /usr/lib64/
$ sudo ldconfig

编译应用程序

从你的应用程序源文件代码(mathDemo.c)中创建一个名称为 mathDemo.o 的目标文件:

$ gcc -I . -c mathDemo.c

-I 选项告诉 GCC 来在其后所列出的目录中搜索头文件(在这个示例中是 mymath.h)。在这个示例中,你指定的是当前目录,通过一个单点(.)来表示。创建一个可执行文件,使用 -l 选项来通过名称来引用你的共享数学库:

$ gcc -o mathDynamic mathDemo.o -lmymath

GCC 会找到 libmymath.so ,因为它存在于一个默认的系统库目录中。使用 ldd 来查证所使用的共享库:

$ ldd mathDemo
    linux-vdso.so.1 (0x00007fffe6a30000)
    libmymath.so => /usr/lib64/libmymath.so (0x00007fe4d4d33000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fe4d4b29000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe4d4d4e000)

看看 mathDemo 可执行文件的大小:

$ du ./mathDynamic
24   ./mathDynamic

当然,它是一个小的应用程序,它所占用的磁盘空间量也反映了这一点。相比之下,相同代码的一个静态链接版本(正如你将在我后期的文章所看到的一样)是 932K !

$ ./mathDynamic
Enter two numbers
25
5

25 + 5 = 30
25 - 5 = 20
25 * 5 = 125
25 / 5 = 5

你可以使用 file 命令来查证它是动态链接的:

$ file ./mathDynamic
./mathDynamic: ELF 64-bit LSB executable, x86-64,
dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
with debug_info, not stripped

成功!

动态链接

因为链接发生在运行时,所以,使用一个共享库会产生一个轻量型的可执行文件。因为它在运行时解析引用,所以它会花费更多的执行时间。不过,因为在日常使用的 Linux 系统上绝大多数的命令是动态链接的,并且在现代硬件上,所能节省的时间是可以忽略不计的。对开发者和用户来说,它的固有模块性是一种强大的功能。

在这篇文章中,我描述了如何创建动态库,并将其链接到一个最终可执行文件。在我的下一篇文章中,我将使用相同的源文件代码来创建一个静态链接的可执行文件。


via: https://opensource.com/article/22/5/dynamic-linking-modular-libraries-linux

作者:Jayashree Huttanagoudar 选题:lkxed 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

编译软件在你如何运行你的系统方面给你很大的灵活性。LD_LIBRARY_PATH 变量,以及 GCC 的 -L-l 选项,是这种灵活性的组成部分。

编译软件是开发者经常做的事情,在开源世界中,一些用户甚至选择自己动手。Linux 播客 Dann Washko 称源码为“通用包格式”,因为它包含了使一个应用在任何平台上运行所需的所有组件。当然,并不是所有的源码都是为所有的系统编写的,所以它只是在目标系统的子集内是“通用”的,但问题是,源码是非常灵活的。有了开源,你可以决定代码的编译和运行方式。

当你在编译代码时,你通常要处理多个源文件。开发人员倾向于将不同的类或模块放在不同的文件中,这样它们可以被单独维护,甚至可能被不同的项目使用。但当你编译这些文件时,许多文件会被编译成一个可执行文件。

这通常是通过创建共享库来完成的,然后从可执行文件中动态链接回它们。这样可以通过保持模块化功能的外部性来保持可执行文件的小型化,并确保库可以独立于使用它们的应用而被更新。

在编译过程中定位一个共享对象

当你 用 GCC 编译 时,你通常需要在你的工作站上安装一个库,以便 GCC 能够定位到它。默认情况下,GCC 假定库在系统库路径中,例如 /lib64/usr/lib64。然而,如果你要链接到一个你自己的尚未安装的库,或者你需要链接到一个没有安装在标准位置的库,那么你必须帮助 GCC 找到这些文件。

有两个选项对于在 GCC 中寻找库很重要:

  • -L(大写字母 L)在 GCC 的搜索位置上增加一个额外的库路径。
  • -l(小写字母 L)设置你要链接的库的名字。

例如,假设你写了一个叫做 libexample.so 的库,并且你想在编译你的应用 demo.c 时使用它。首先,从 demo.c 创建一个对象文件:

$ gcc -I ./include -c src/demo.c

-I 选项在 GCC 搜索头文件的路径中增加了一个目录。在这个例子中,我假设自定义头文件在一个名为 include 的本地目录中。-c 选项防止 GCC 运行链接器,因为这个任务只是为了创建一个对象文件。结果如下:

$ ls
demo.o   include/   lib/    src/

现在你可以使用 -L 选项为你的库设置一个路径,然后进行编译:

$ gcc -L`pwd`/lib -o myDemo demo.o -lexample

注意,-L 选项在 -l 选项之前。这很重要,因为如果在你告诉 GCC 查找非默认库之前没有将 -L 添加到 GCC 的搜索路径中,GCC 就不知道要在你的自定义位置上搜索。编译成功了,但当你试图运行它时,却出现了问题:

$ ./myDemo
./myDemo: error while loading shared libraries:
libexample.so: cannot open shared object file:
No such file or directory

用 ldd 排除故障

ldd 工具可以打印出共享对象的依赖关系,它在排除类似问题时很有用:

$ ldd ./myDemo
        linux-vdso.so.1 (0x00007ffe151df000)
        libexample.so => not found
        libc.so.6 => /lib64/libc.so.6 (0x00007f514b60a000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f514b839000)

你已经知道定位不到 libexample,但 ldd 输出至少确认了它对工作库的期望位置。例如,libc.so.6已经被定位,ldd 显示其完整路径。

LD\_LIBRARY\_PATH

LD_LIBRARY_PATH 环境变量 定义了库的路径。如果你正在运行一个依赖于没有安装到标准目录的库的应用程,你可以使用 LD_LIBRARY_PATH 添加到系统的库搜索路径。

有几种设置环境变量的方法,但最灵活的是在运行命令前放置环境变量。看看设置 LD_LIBRARY_PATHldd 命令在分析一个“损坏”的可执行文件时的作用:

$ LD_LIBRARY_PATH=`pwd`/lib ldd ./
   linux-vdso.so.1 (0x00007ffe515bb000)
   libexample.so => /tmp/Demo/lib/libexample.so (0x0000...
   libc.so.6 => /lib64/libc.so.6 (0x00007eff037ee000)
   /lib64/ld-linux-x86-64.so.2 (0x00007eff03a22000)

这也同样适用于你的自定义命令:

$ LD_LIBRARY_PATH=`pwd`/lib myDemo
hello world!

然而,如果你移动库文件或可执行文件,它又会失效:

$ mv lib/libexample.so ~/.local/lib64
$ LD_LIBRARY_PATH=`pwd`/lib myDemo
./myDemo: error while loading shared libraries...

要修复它,你必须调整 LD_LIBRARY_PATH 以匹配库的新位置:

$ LD_LIBRARY_PATH=~/.local/lib64 myDemo
hello world!

何时使用 LD\_LIBRARY\_PATH

在大多数情况下,LD_LIBRARY_PATH 不是你需要设置的变量。按照设计,库安装到 /usr/lib64 中,因此应用自然会在其中搜索所需的库。在两种情况下,你可能需要使用 LD_LIBRARY_PATH

  • 你正在编译的软件需要链接到本身刚刚编译但尚未安装的库。良好设计的构建系统,例如 AutotoolsCMake,可以帮助处理这个问题。
  • 你正在使用设计为在单个目录之外运行的软件,它没有安装脚本,或安装脚本将库放置在非标准目录中。一些应用具有 Linux 用户可以下载、复制到 /opt 并在“不安装”的情况下运行的版本。LD_PATH_LIBRARY 变量是通过封装脚本设置的,因此用户通常甚至不知道它已被设置。

编译软件为你在运行系统方面提供了很大的灵活性。LD_LIBRARY_PATH 变量以及 -L-l GCC 选项是这种灵活性的组成部分。


via: https://opensource.com/article/22/5/compile-code-ldlibrarypath

作者:Seth Kenlon 选题:lkxed 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出