通过理解解释、即时编译和预先编译之间的区别，有效地使用它们。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 命令行使用经验。本文更注重亲身实践的体验。不管怎么说，我仍建议你在虚拟机或备用系统中尝试此冒险，而非在你的主系统上进行。前置准备在软件领域，构建任何事物都有两个基本要求：源代码构建依赖因此，作为预备环节，我们需要下载 Linux 内核的源码压缩包，并安装一些能让我们成功构建 Linux 内核的依赖项。Linux 版本导览在任何时刻，Freax Linux 内核都有四种“版本”。Linux 的这些 “版本”，按照开发流程的顺序是：linux-next 树： 所有准备合并到 Linux 代码库的代码首先被合并到 linux-next 树。它代表的是 Linux 内核最新也是“最不稳定”的状态。大多数 Linux 内核开发者和测试人员使用这个来提高代码质量，为 Linus Torvalds 的后续提取做准备。请谨慎使用！发布候选版（RC） / 主线版： Linus 从 linux-next 树抽取代码并创建一个初始发布版本。这个初始发布版本的测试版称为 RC（ 发布候选 Release Candidate ）版本。一旦 RC 版本发布，Linus 只会接受对它的错误修复和性能退化相关的补丁。基础这些反馈，Linus 会每周发布一个 RC 内核，直到他对代码感到满意。RC 发行版本的标识是 -rc 后缀，后面跟一个数字。稳定版： 当 Linus 觉得最新的 RC 版本已稳定时，他会发布最终的“公开”版本。稳定发布版将会维护几周时间。像 Arch Linux 和 Fedora Linux 这样的前沿 Linux 发行版会使用此类版本。我建议你在试用 linux-next 或任何 RC 版本之前，先试一试此版本。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 torvalds@kernel.org gregkh@kernel.org 你应该可以得到一个与我在我的电脑上看到的类似的结果：$ gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org gpg: /home/pratham/.gnupg/trustdb.gpg: trustdb created gpg: key 38DBBDC86092693E: public key "Greg Kroah-Hartman <gregkh@kernel.org>" imported gpg: Total number processed: 1 gpg: imported: 1 gpg: key 79BE3E4300411886: public key "Linus Torvalds <torvalds@kernel.org>" imported gpg: Total number processed: 1 gpg: imported: 1 pub rsa4096 2011-09-23 [SC] 647F28654894E3BD457199BE38DBBDC86092693E uid [ unknown] Greg Kroah-Hartman <gregkh@kernel.org> sub rsa4096 2011-09-23 [E] pub rsa2048 2011-09-20 [SC] ABAF11C65A2970B130ABE3C479BE3E4300411886 uid [ unknown] Linus Torvalds <torvalds@kernel.org> 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 <gregkh@kernel.org>" [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 内核的源码。这个步骤十分简单，只需对 Tar 文件执行 tar -xf 命令，如下：tar -xf linux-*.tar 在这里，-x 选项表示解压，-f 选项则用来告诉 Tar 文件的文件名。这个解压过程可能需要几分钟时间，你可以先放松，耐心等待一下。配置 Linux 内核Linux 内核的构建过程会查找 .config 文件。顾名思义，这是一个配置文件，用于指定 Linux 内核的所有可能的配置选项。这是必需的文件。获取 Linux 内核的 .config 文件有两种方式：使用你的 Linux 发行版的配置作为基础（推荐做法）使用默认的，通用的配置? 也有第三种方法，也就是从零开始，手动配置每一个选项，但注意，这需要配置超过 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 目标只会构建少数项目，构建时间将会缩短。我个人选择它的原因主要有：检查我在代码/工具链中做的修改是否正确，以及代码是否可以编译。在虚拟机中只进行少数选项的测试。? 在为 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 文件，你都可能希望熟悉如何修改这个配置文件。最可靠的修改方式是使用 menuconfig 或 nconfig 目标。这两个目标的功能是相同的，只不过提供给你的界面有所不同。这是这两者间唯一的区别。我个人更偏向于使用 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 语言程序的生命周期：带有宏定义的 C 源代码 .c 文件，用 cpp 预处理为 .i 文件。扩展了宏定义的 C 源代码 .i 文件，会被 gcc 转译成 .s 文件。以汇编语言写的文本文件 .s 文件被汇编为目标 .o 文件。带有 CPU 指令的二进制目标代码，以及其他目标文件和库 *.o 文件，以内存区域无关的偏移量，使用 ld 链接以生成可执行文件。最终的二进制文件要么包含所有需要的目标，要么设置以动态链接库 *.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.c 、sub.c 、mult.c 和 divi.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.o 、sub.o 、mult.o 和 divi.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.o 和 libmymath.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.c 、sub.c 、mult.c 和 divi.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.o、sub.o、mult.o 和 divi.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， 然后运行 ldconfig 。ldconfig 命令创建所需的链接，并缓存到标准库目录中发现的最新共享库。$ 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\_PATHLD_LIBRARY_PATH 环境变量 定义了库的路径。如果你正在运行一个依赖于没有安装到标准目录的库的应用程，你可以使用 LD_LIBRARY_PATH 添加到系统的库搜索路径。有几种设置环境变量的方法，但最灵活的是在运行命令前放置环境变量。看看设置 LD_LIBRARY_PATH 对 ldd 命令在分析一个“损坏”的可执行文件时的作用：$ 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：你正在编译的软件需要链接到本身刚刚编译但尚未安装的库。良好设计的构建系统，例如 Autotools 和 CMake，可以帮助处理这个问题。你正在使用设计为在单个目录之外运行的软件，它没有安装脚本，或安装脚本将库放置在非标准目录中。一些应用具有 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中国 荣誉推出
这是一篇如何从源码编译 GNOME 的快速指南，包括 Shell、mutter 和一些原生应用。在编译之前，你需要确保一些事情，因为以下编译直接来自 Gitlab 的主分支，其中包含一些开发包。通常，你可以选择在任何 Linux 发行版中编译。但是我建议使用 Fedora Rawhide（Fedora 的开发分支，用于将来的发布）。另外，请勿在稳定系统中尝试此操作。因为操作可能出错，所以你可能最终得到损坏的系统。总而言之，你需要以下内容来从源码编译 GNOME。测试环境（虚拟机 或测试系统）。Fedora Rawhide 发行版（推荐，从此处下载）。确保你的发行版是最新的。你已登录 X.org 会话。我不建议你在 Wayland 会话中进行编译，因为你会遇到问题。从源码编译 GNOMEGNOME 桌面是一个基于其功能的软件包集合。Linux 发行版的桌面组件工作于窗口管理器和 shell 之下。因此，对于 GNOME，我将首先编译 mutter – 它是 GNOME Shell 的窗口管理器。然后进行 GNOME Shell 的编译。最后，我将编译一些原生应用。我将使用 meson 构建系统进行编译。meson 是一个漂亮的构建系统，快速且用户友好。编译 mutter打开终端并安装 GNOME Shell 和 mutter 所需的软件包。sudo dnf build-dep mutter gnome-shell 在主目录（或你想要的任何地方）中创建演示目录。cd ~ mkdir demo cd demo 从 Gitlab 克隆 mutter 的主分支。git clone https://gitlab.gnome.org/GNOME/mutter 进入克隆目录，然后使用以下 meson 命令来准备构建文件。默认情况下，meson 使用 /usr/local 用于构建文件。但是，你也可以使用前缀开关将输出重定向到特定文件夹（如下所示）。cd mutter meson _build --prefix=/usr 使用以下命令在构建完成时，将 mutter 安装在到系统中。sudo ninja install -C _build 编译 GNOME ShellGNOME Shell 和其他软件包的编译方法类似。首先，从 GitLab 克隆 GNOME Shell 主仓库，然后进行编译和安装。你可以按照下面的命令依次进行。在 GNOME Shell 中，你需要两个依赖项。它们是 asciidoc 和 sassc 。请在构建 GNOME Shell 之前安装它们。sudo dnf install asciidoc sudo dnf install sassc 安装完这些依赖项后，按照下面的命令来构建和安装 GNOME Shell。在运行这个命令之前，请确保你回到 demo 文件夹（我在第一步创建的）。git clone https://gitlab.gnome.org/GNOME/gnome-shellcd gnome-shellmeson _build --prefix=/usrsudo ninja install -C _build 运行 GNOME Shell编译完成后，你可以尝试重新启动 GNOME Shell 来查看来自主分支的变化。在重启之前，正如我之前提到的，确保你处于 X.Org 会话中。按 ALT+F2 并输入 r。然后按回车键。这个命令将重启 GNOME Shell。恭喜你! 你已经成功地编译了 GNOME Shell 和 Mutter。现在，是时候编译一些 GNOME 原生应用了。编译 GNOME 原生应用这些步骤对于 GNOME 或任何应用的所有源码都是一样的。你需要改变仓库的名字。因此，这里有一些编译必要的 GNOME 原生应用的命令示例。Files（Nautilus）git clone https://gitlab.gnome.org/GNOME/nautilus/cd gnome-shellmeson _build --prefix=/usrsudo ninja install -C _build GNOME 软件商店git clone https://gitlab.gnome.org/GNOME/gnome-software/cd gnome-shellmeson _build --prefix=/usrsudo ninja install -C _build GNOME 控制中心git clone https://gitlab.gnome.org/GNOME/gnome-control-center/cd gnome-shellmeson _build --prefix=/usrsudo ninja install -C _build FAQ使用上述步骤，你可以编译任何源码分支。不仅仅是 GNOME。GitLab 服务器有时很慢，克隆一个仓库可能需要较长的时间。如果 git clone 失败，我建议你再试一次。结束语我希望这个小小的高级教程能够帮助你在新的 GNOME 功能出现在 GNOME 每日构建系统之前尝试它。既然你编译了，你也可以为测试新的 GNOME 功能做出贡献，并在 GitLab 问题页面上报告任何特定包的 bug 或问题。这篇文章是开源应用编译系列的第一篇文章。请继续关注更多开源应用的编译文章。另外，请让我在下面的评论栏中知道你的评论、建议，或者你在使用这些说明时遇到的任何错误。干杯。via: https://www.debugpoint.com/2022/05/compile-gnome-source/作者：Arindam 选题：lkxed 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
带你一窥生成二进制文件步骤的幕后，以便在出现一些错误时，你知道如何逐步解决问题。C 语言广为人知，深受新老程序员的好评。使用 C 语言编写的源文件代码，使用了标准的英语术语，因而人们可以方便阅读。然而，计算机只能理解二进制代码。为将代码转换为机器语言，你需要使用一种被称为 编译器 compiler 的工具。最常见的编译器是 GCC（ GNU 编译器集 GNU Compiler Collection ）。编译过程涉及到一系列的中间步骤及相关工具。安装 GCC为验证在你的系统上是否已经安装了 GCC，使用 gcc 命令：$ gcc --version 如有必要，使用你的软件包管理器来安装 GCC。在基于 Fedora 的系统上，使用 dnf ：$ sudo dnf install gcc libgcc 在基于 Debian 的系统上，使用 apt ：$ sudo apt install build-essential 在安装后，如果你想查看 GCC 的安装位置，那么使用：$ whereis gcc 演示使用 GCC 来编译一个简单的 C 程序这里有一个简单的 C 程序，用于演示如何使用 GCC 来编译。打开你最喜欢的文本编辑器，并在其中粘贴这段代码：// hellogcc.c #include <stdio.h> int main() { printf("Hello, GCC!\n"); return 0; } 保存文件为 hellogcc.c ，接下来编译它：$ ls hellogcc.c $ gcc hellogcc.c $ ls -1 a.out hellogcc.c 如你所见，a.out 是编译后默认生成的二进制文件。为查看你所新编译的应用程序的输出，只需要运行它，就像你运行任意本地二进制文件一样：$ ./a.out Hello, GCC! 命名输出的文件文件名称 a.out 是非常莫名其妙的，所以，如果你想具体指定可执行文件的名称，你可以使用 -o 选项：（LCTT 译注：注意这和最近 Linux 内核废弃的 a.out 格式无关，只是名字相同，这里生成的 a.out 是 ELF 格式的 —— 也不知道谁给起了个 a.out 这破名字，在我看来，默认输出文件名就应该是去掉了 .c 扩展名后的名字。by wxy）$ gcc -o hellogcc hellogcc.c $ ls a.out hellogcc hellogcc.c $ ./hellogcc Hello, GCC! 当开发一个需要编译多个 C 源文件文件的大型应用程序时，这种选项是很有用的。在 GCC 编译中的中间步骤编译实际上有四个步骤，即使在简单的用例中 GCC 自动执行了这些步骤。 预处理 Pre-Processing ：GNU 的 C 预处理器（cpp）解析头文件（#include 语句），展开 宏 macros 定义（#define 语句），并使用展开的源文件代码来生成一个中间文件，如 hellogcc.i。 编译 Compilation ：在这个期间中，编译器将预处理的源文件代码转换为指定 CPU 架构的汇编代码。由此生成是汇编文件使用一个 .s 扩展名来命名，如在这个示例中的 hellogcc.s 。 汇编 Assembly ：汇编程序（as）将汇编代码转换为目标机器代码，放在目标文件中，例如 hellogcc.o 。 链接 Linking ：链接器（ld）将目标代码和库代码链接起来生成一个可执行文件，例如 hellogcc 。在运行 GCC 时，可以使用 -v 选项来查看每一步的细节：$ gcc -v -o hellogcc hellogcc.c 手动编译代码体验编译的每个步骤可能是很有用的，因此在一些情况下，你不需要 GCC 完成所有的步骤。首先，除源文件文件以外，删除在当前文件夹下生成的文件。$ rm a.out hellogcc.o $ ls hellogcc.c 预处理器首先，启动预处理器，将其输出重定向为 hellogcc.i ：$ cpp hellogcc.c > hellogcc.i $ ls hellogcc.c hellogcc.i 查看输出文件，并注意一下预处理器是如何包含头文件和扩展宏中的源文件代码的。编译器现在，你可以编译代码为汇编代码。使用 -S 选项来设置 GCC 只生成汇编代码：$ gcc -S hellogcc.i $ ls hellogcc.c hellogcc.i hellogcc.s $ cat hellogcc.s 查看汇编代码，来看看生成了什么。汇编使用你刚刚所生成的汇编代码来创建一个目标文件：$ as -o hellogcc.o hellogcc.s $ ls hellogcc.c hellogcc.i hellogcc.o hellogcc.s 链接要生成一个可执行文件，你必须将对象文件链接到它所依赖的库。这并不像前面的步骤那么简单，但它却是有教育意义的：$ ld -o hellogcc hellogcc.o ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 ld: hellogcc.o: in function `main`: hellogcc.c:(.text+0xa): undefined reference to `puts' 在链接器查找完 libc.so 库后，出现一个引用 undefined puts 错误。你必须找出适合的链接器选项来链接必要的库以解决这个问题。这不是一个小技巧，它取决于你的系统的布局。在链接时，你必须链接代码到 核心运行时 core runtime （CRT）目标，这是一组帮助二进制可执行文件启动的子例程。链接器也需要知道在哪里可以找到重要的系统库，包括 libc 和 libgcc，尤其是其中的特殊的开始和结束指令。这些指令可以通过 --start-group 和 --end-group 选项来分隔，或者使用指向 crtbegin.o 和 crtend.o 的路径。这个示例使用了 RHEL 8 上的路径，因此你可能需要依据你的系统调整路径。$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \ -o hello \ /usr/lib64/crt1.o /usr/lib64/crti.o \ --start-group \ -L/usr/lib/gcc/x86_64-redhat-linux/8 \ -L/usr/lib64 -L/lib64 hello.o \ -lgcc \ --as-needed -lgcc_s \ --no-as-needed -lc -lgcc \ --end-group \ /usr/lib64/crtn.o 在 Slackware 上，同样的链接过程会使用一组不同的路径，但是，你可以看到这其中的相似之处：$ ld -static -o hello \ -L/usr/lib64/gcc/x86_64-slackware-linux/11.2.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, GCC! 一些有用的实用程序下面是一些帮助检查文件类型、 符号表 symbol tables 和链接到可执行文件的库的实用程序。使用 file 实用程序可以确定文件的类型：$ file hellogcc.c hellogcc.c: C source, ASCII text $ file hellogcc.o hellogcc.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $ file hellogcc hellogcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb76b241d7d00871806e9fa5e814fee276d5bd1a, for GNU/Linux 3.2.0, not stripped 对目标文件使用 nm 实用程序可以列出 符号表 symbol tables ：$ nm hellogcc.o 0000000000000000 T main U puts 使用 ldd 实用程序来列出动态链接库：$ ldd hellogcc linux-vdso.so.1 (0x00007ffe3bdd7000) libc.so.6 => /lib64/libc.so.6 (0x00007f223395e000) /lib64/ld-linux-x86-64.so.2 (0x00007f2233b7e000) 总结在这篇文章中，你了解到了 GCC 编译中的各种中间步骤，和检查文件类型、 符号表 symbol tables 和链接到可执行文件的库的实用程序。在你下次使用 GCC 时，你将会明白它为你生成一个二进制文件所要做的步骤，并且当出现一些错误时，你会知道如何逐步处理解决问题。via: https://opensource.com/article/22/5/gnu-c-compiler作者：Jayashree Huttanagoudar 选题：lkxed 译者：robsean 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这些方便的 Go 构建选项可以帮助你更好地理解 Go 的编译过程。学习一门新的编程语言最令人欣慰的部分之一，就是最终运行了一个可执行文件，并获得预期的输出。当我开始学习 Go 这门编程语言时，我先是阅读一些示例程序来熟悉语法，然后是尝试写一些小的测试程序。随着时间的推移，这种方法帮助我熟悉了编译和构建程序的过程。Go 的构建选项提供了更好地控制构建过程的方法。它们还可以提供额外的信息，帮助把这个过程分成更小的部分。在这篇文章中，我将演示我所使用的一些选项。注意：我使用的“ 构建 build ”和“ 编译 compile ”这两个词是同一个意思。开始使用 Go我使用的 Go 版本是 1.16.7。但是，这里给出的命令应该也能在最新的版本上运行。如果你没有安装 Go，你可以从 Go 官网 上下载它，并按照说明进行安装。你可以通过打开一个命令提示符，并键入下面的命令来验证你所安装的版本：$ go version 你应该会得到类似下面这样的输出，具体取决于你安装的版本：go version go1.16.7 linux/amd64 基本的 Go 程序的编译和执行方法我将从一个在屏幕上简单打印 “Hello World” 的 Go 程序示例开始，就像下面这样：$ cat hello.go package main import "fmt" func main() { fmt.Println("Hello World") } 在讨论更高级的选项之前，我将解释如何编译这个 Go 示例程序。我使用了 build 命令，后面跟着 Go 程序的源文件名，本例中是 hello.go，就像下面这样：$ go build hello.go 如果一切工作正常，你应该看到在你的当前目录下创建了一个名为 hello 的可执行文件。你可以通过使用 file 命令验证它是 ELF 二进制可执行格式（在 Linux 平台上）。你也可以直接执行它，你会看到它输出 “Hello World”。$ ls hello hello.go $ file ./hello ./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ ./hello Hello World Go 提供了一个方便的 run 命令，以便你只是想看看程序是否能正常工作，并获得预期的输出，而不想生成一个最终的二进制文件。请记住，即使你在当前目录中没有看到可执行文件，Go 仍然会在某个地方编译并生成可执行文件并运行它，然后把它从系统中删除。我将在本文后面的章节中解释。$ go run hello.go Hello World $ ls hello.go 更多细节上面的命令就像一阵风一样，一下子就运行完了我的程序。然而，如果你想知道 Go 在编译这些程序的过程中做了什么，Go 提供了一个 -x 选项，它可以打印出 Go 为产生这个可执行文件所做的一切。简单看一下你就会发现，Go 在 /tmp 内创建了一个临时工作目录，并生成了可执行文件，然后把它移到了 Go 源程序所在的当前目录。$ go build -x hello.go WORK=/tmp/go-build1944767317 mkdir -p $WORK/b001/ << snip >> mkdir -p $WORK/b001/exe/ cd . /usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK \ /b001/exe/a.out -importcfg $WORK/b001 \ /importcfg.link -buildmode=exe -buildid=K26hEYzgDkqJjx2Hf-wz/\ nDueg0kBjIygx25rYwbK/W-eJaGIOdPEWgwC6o546 \ /K26hEYzgDkqJjx2Hf-wz -extld=gcc /root/.cache/go-build /cc \ /cc72cb2f4fbb61229885fc434995964a7a4d6e10692a23cc0ada6707c5d3435b-d /usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK \ /b001/exe/a.out # internal mv $WORK/b001/exe/a.out hello rm -r $WORK/b001/ 这有助于解决在程序运行后却在当前目录下没有生成可执行文件的谜团。使用 -x 显示可执行文件确实在 /tmp 工作目录下创建并被执行了。然而，与 build 命令不同的是，可执行文件并没有移动到当前目录，这使得看起来没有可执行文件被创建。$ go run -x hello.go mkdir -p $WORK/b001/exe/ cd . /usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001 \ /exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=hK3wnAP20DapUDeuvAAS/E_TzkbzwXz6tM5dEC8Mx \ /7HYBzuaDGVdaZwSMEWAa/hK3wnAP20DapUDeuvAAS -extld=gcc \ /root/.cache/go-build/75/ \ 7531fcf5e48444eed677bfc5cda1276a52b73c62ebac3aa99da3c4094fa57dc3-d $WORK/b001/exe/hello Hello World 模仿编译而不产生可执行文件假设你不想编译程序并产生一个实际的二进制文件，但你确实想看到这个过程中的所有步骤。你可以通过使用 -n 这个构建选项来做到这一点，该选项会打印出通常的执行步骤，而不会实际创建二进制文件。$ go build -n hello.go 保存临时目录很多工作都发生在 /tmp 工作目录中，一旦可执行文件被创建和运行，它就会被删除。但是如果你想看看哪些文件是在编译过程中创建的呢？Go 提供了一个 -work 选项，它可以在编译程序时使用。-work 选项除了运行程序外，还打印了工作目录的路径，但它并不会在这之后删除工作目录，所以你可以切换到该目录，检查在编译过程中创建的所有文件。$ go run -work hello.go WORK=/tmp/go-build3209320645 Hello World $ find /tmp/go-build3209320645 /tmp/go-build3209320645 /tmp/go-build3209320645/b001 /tmp/go-build3209320645/b001/importcfg.link /tmp/go-build3209320645/b001/exe /tmp/go-build3209320645/b001/exe/hello $ /tmp/go-build3209320645/b001/exe/hello Hello World 其他编译选项如果说，你想手动编译程序，而不是使用 Go 的 build 和 run 这两个方便的命令，最后得到一个可以直接由你的操作系统（这里指 Linux）运行的可执行文件。那么，你该怎么做呢？这个过程可以分为两部分：编译和链接。你可以使用 tool 选项来看看它是如何工作的。首先，使用 tool compile 命令产生结果的 ar 归档文件，它包含了 .o 中间文件。接下来，对这个 hello.o 文件执行 tool link 命令，产生最终的可执行文件，然后你就可以运行它了。$ go tool compile hello.go $ file hello.o hello.o: current ar archive $ ar t hello.o __.PKGDEF _go_.o $ go tool link -o hello hello.o $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ ./hello Hello World 如果你想进一步查看基于 hello.o 文件产生可执行文件的链接过程，你可以使用 -v 选项，它会搜索每个 Go 可执行文件中包含的 runtime.a 文件。$ go tool link -v -o hello hello.o HEADER = -H5 -T0x401000 -R0x1000 searching for runtime.a in /usr/lib/golang/pkg/linux_amd64/runtime.a 82052 symbols, 18774 reachable 1 package symbols, 1106 hashed symbols, 77185 non-package symbols, 3760 external symbols 81968 liveness data 交叉编译选项现在我已经解释了 Go 程序的编译过程，接下来，我将演示 Go 如何通过在实际的 build 命令之前提供 GOOS 和 GOARCH 这两个环境变量，来允许你构建针对不同硬件架构和操作系统的可执行文件。这有什么用呢？举个例子，你会发现为 ARM（arch64）架构制作的可执行文件不能在英特尔（x86\_64）架构上运行，而且会产生一个 Exec 格式错误。下面的这些选项使得生成跨平台的二进制文件变得小菜一碟：$ GOOS=linux GOARCH=arm64 go build hello.go $ file ./hello ./hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped $ ./hello bash: ./hello: cannot execute binary file: Exec format error $ uname -m x86_64 你可以阅读我之前的博文，以更多了解我在 使用 Go 进行交叉编译 方面的经验。查看底层汇编指令源代码并不会直接转换为可执行文件，尽管它生成了一种中间汇编格式，然后最终被组装为可执行文件。在 Go 中，这被映射为一种中间汇编格式，而不是底层硬件汇编指令。要查看这个中间汇编格式，请在使用 build 命令时，提供 -gcflags 选项，后面跟着 -S。这个命令将会显示使用到的汇编指令：$ go build -gcflags="-S" hello.go # command-line-arguments "".main STEXT size=138 args=0x0 locals=0x58 funcid=0x0 0x0000 00000 (/test/hello.go:5) TEXT "".main(SB), ABIInternal, $88-0 0x0000 00000 (/test/hello.go:5) MOVQ (TLS), CX 0x0009 00009 (/test/hello.go:5) CMPQ SP, 16(CX) 0x000d 00013 (/test/hello.go:5) PCDATA $0, $-2 0x000d 00013 (/test/hello.go:5) JLS 128 << snip >> 你也可以使用 objdump -s 选项，来查看已经编译好的可执行程序的汇编指令，就像下面这样：$ ls hello hello.go $ go tool objdump -s main.main hello TEXT main.main(SB) /test/hello.go hello.go:5 0x4975a0 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX hello.go:5 0x4975a9 483b6110 CMPQ 0x10(CX), SP hello.go:5 0x4975ad 7671 JBE 0x497620 hello.go:5 0x4975af 4883ec58 SUBQ $0x58, SP hello.go:6 0x4975d8 4889442448 MOVQ AX, 0x48(SP) << snip >> 分离二进制文件以减少其大小Go 的二进制文件通常比较大。例如, 一个简单的 “Hello World” 程序将会产生一个 1.9M 大小的二进制文件。$ go build hello.go $ $ du -sh hello 1.9M hello $ $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ 为了减少生成的二进制文件的大小，你可以分离执行过程中不需要的信息。使用 -ldflags 和 -s -w 选项可以使生成的二进制文件略微变小为 1.3M。$ go build -ldflags="-s -w" hello.go $ $ du -sh hello 1.3M hello $ $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped $ 总结我希望这篇文章向你介绍了一些方便的 Go 编译选项，同时帮助了你更好地理解 Go 编译过程。关于构建过程的其他信息和其他有趣的选项，请参考 Go 命令帮助：$ go help build题图由 Ashraf Chemban 在 Pixabay 上发布。via: https://opensource.com/article/22/4/go-build-options作者：Gaurav Kamathe 选题：lkxed 译者：lkxed 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
任何人都可以通过这三个简单的步骤编译开源代码
你不需要知道如何写或读代码就可以编译它。安装软件的方法有很多，但开源让你有了一个其他地方所没有的选择：你可以自己编译代码。编译源代码的经典三步流程是：$ ./configure $ make $ sudo make install 由于有了这些命令，你可能会惊讶地发现，你不需要知道如何写代码，甚至不需要读代码就可以编译它。安装用来构建软件的命令由于这是你第一次进行编译，所以有一个一次性的准备步骤，即安装用于构建软件的命令。具体来说，你需要一个编译器。编译器（比如 GCC 或 LLVM）可以将像这样的源代码：#include <iostream> using namespace std; int main() { cout << "hello world"; } 变成 机器语言，即 CPU 用来处理信息的指令。你可以看一下机器代码，但它对你来说没有任何意义（除非你是一个 CPU）。你可以使用你的软件包管理器安装 GNU 编译器集合（GCC）和 LLVM 编译器，以及其他在 Fedora、CentOS、Mageia 和类似发行版上进行编译的基本命令：$ sudo dnf install @development clang 在 Debian、Elementary、Mint 和类似发行版上命令如下：$ sudo apt install build-essential clang 在你的系统设置好后，有几项你每次编译软件时都要重复进行的任务：下载源代码展开源代码的存档文件编译你已经有了所有你需要的命令，所以现在你需要一些软件来编译。1、下载源代码获得一个应用程序的源代码和获得任何可下载的软件一样。你得去一个网站或一个代码管理网站，如 GitLab、SourceForge 或 GitHub。 通常情况下，开源软件既有正在进行的工作（“ 当前 current ”或 “ 每夜 nightly ”）的构建版本，也有打包的“ 稳定 stable ”发布版本。在可能的情况下，使用稳定版本，除非你有理由相信，或者对代码有足够的了解，能够在出现故障时修复。术语“稳定版”表明这些代码经过了测试，而且该应用程序的程序员对代码有足够的信心，从而将其打包成 .zip 或 .tar 归档，给了它一个官方编号，有时还有一个发布名称，然后提供给一般的非程序员公众下载。在这个练习中，我使用 Angband，一个开源的（GPLv2）ASCII 地牢猎手游戏。这是一个简单的应用程序，其复杂程度刚好可以说明你在自己编译软件时需要考虑的问题。从 网站 上下载其源代码。2、展开源代码的存档文件源代码通常是以存档的形式交付的，因为源代码通常由多个文件组成的。在与之交互之前，你必须先解压，不管它是一个 tarball、一个 zip 文件、一个 7z 文件，还是其他完全不同的东西。$ tar --extract --file Angband-x.y.z.tar.gz 一旦解压缩后，就把目录切换到解压缩的目录，然后看一看。通常在目录的顶层有一个 README 文件。这个文件，一般会包含你需要做什么来编译代码的指导。README 通常包含关于代码的这些重要方面的信息：语言：代码使用的是什么语言（例如，C、C++、Rust、Python）。依赖性：你需要在你的系统上安装其他什么的软件，以便这个应用程序能够构建和运行。说明：你构建该软件所需要采取的明确步骤。偶尔，他们会在一个专门的文件中包含这些信息，这个文件被直观地称为 INSTALL。如果 README 文件中不包含这些信息，可以考虑向开发者提交一份错误报告。你不是唯一需要介绍一下源代码的人。不管他们有多么丰富的经验，每个人都会对从未见过的源代码感到陌生，而文档是很重要的！Angband 的维护者给出了在线说明的链接，描述了如何编译代码。这份文件还描述了你需要安装哪些其他软件，尽管它并没有确切地说明这一点。该网站说，“有几个不同的可选构建的前端（GCU、SDL、SDL2 和 X11），你可以使用诸如 --enable-sdl，--disable-x11 的参数配置。”这可能对你来说看起来像天书，但你经常编译代码后就会习惯。无论你是否理解 X11 或 SDL2 是什么，它们都是你经过几个月定期编译代码后经常看到的要求。你会对大多数软件需要其他软件库的想法感到适应，因为它们建立在其他技术之上。不过在这种情况下，Angband 非常灵活，无论是否有这些可选的依赖，都可以进行编译，所以现在，你可以假装没有额外的依赖。3、编译代码构建代码的典型步骤是：$ ./configure $ make $ sudo make install 这些是使用 Autotools 构建的项目的步骤，该框架是为了规范源代码的交付方式而创建的。然而，还有一些其他框架（如 Cmake），它们需要不同的步骤。当项目没有遵循 Autotools 或 Cmake 框架时，它们往往会在 README 文件中提醒你。配置Angband 使用 Autotools，所以现在是编译代码的时候了!在 Angband 目录中，首先，运行随源码一起提供的配置脚本：$ ./configure 这一步将扫描你的系统，找到 Angband 正确构建所需的依赖性。有些依赖是非常基本的，没有它们你的电脑就无法运行，而有些则是专门的。在这一过程结束时，该脚本会给你一份关于它所发现的东西的报告：[...] configure: creating ./config.status config.status: creating mk/buildsys.mk config.status: creating mk/extra.mk config.status: creating src/autoconf.h Configuration: Install path: /usr/local binary path: /usr/local/games config path: /usr/local/etc/angband/ lib path: /usr/local/share/angband/ doc path: /usr/local/share/doc/angband/ var path: (not used) (save and score files in ~/.angband/Angband/) -- Frontends -- - Curses Yes - X11 Yes - SDL2 Disabled - SDL Disabled - Windows Disabled - Test No - Stats No - Spoilers Yes - SDL2 sound Disabled - SDL sound Disabled 有些输出可能对你有意义，有些可能没有。无论如何，你可能注意到 SDL2 和 SDL 被标记为 “Disabled”，Test 和 Stats 都被标记为 “None”。虽然这些信息是负面的，但这并不一定是一件坏事。从本质上讲，这就是警告和错误之间的区别。如果配置脚本遇到了会阻止它构建代码的东西，它就会用一个错误来提醒你。如果你想稍微优化一下你的构建，你可以选择解决这些负面信息。通过搜索 Angband 文档，你可能会确定 Test 和 Stats 实际上并不是你感兴趣的（它们是 Angband 专用于开发者的选项）。然而，通过在线研究，你可能会发现 SDL2 将是一个很好的功能。要解决编译代码时的依赖问题，你需要安装缺少的组件和该缺少的组件的 开发库。换句话说，Angband 需要 SDL2 来播放声音，但它需要 SDL2-devel（在 Debian 系统上称为 libsdl2-dev）来构建。用你的软件包管理器安装这两个组件：$ sudo dnf install sdl2 sdl2-devel 再试一下配置脚本：$ ./configure --enable-sdl2 [...] Configuration: [...] - Curses Yes - X11 Yes - SDL2 Yes - SDL Disabled - Windows Disabled - Test No - Stats No - Spoilers Yes - SDL sound Disabled - SDL2 sound Yes 制作（编译）一旦一切配置完毕，运行 make 命令：$ make 这通常需要一段时间，但它提供了很多视觉反馈，所以你会知道代码正在被编译。安装最后一步是安装你刚刚编译的代码。安装代码并没有什么神奇之处。所做的就是复制很多文件到非常具体的目录中。无论你是从源代码编译还是运行花哨的图形安装向导，都是如此。由于这些代码会被复制到系统级目录，你必须有 root（管理）权限，这是由 sudo 命令授予的。$ sudo make install 运行该应用程序一旦应用程序被安装，你就可以运行它。根据 Angband 文档，启动游戏的命令是 angband，所以可以试试：$ angband 编译代码无论是在我的 Slackware 台式电脑上，还是在我的 CentOS 笔记本电脑上，我都会使用 NetBSD 的 pkgsrc 系统编译我自己的大部分应用程序。我发现，通过自己编译软件，我可以对应用程序中包含的功能、如何配置、使用的库版本等有自己的想法。这很有意义，它帮助我跟上了新的版本，而且因为我有时会在这个过程中发现错误，它帮助我参与了很多不同的开源项目。你很少会只有编译软件的一种方式可选，大多数开源项目同时提供源代码（这就是为什么它被称为“开源”）和可安装包。是否从源代码编译是你自己的选择，也许是因为你想要最新版本中还没有的新功能，或者只是因为你喜欢自己编译代码。家庭作业Angband 可以使用 Autotools 或 Cmake，所以如果你想体验另一种构建代码的方式，可以试试这个：$ mkdir build $ cd build $ cmake .. $ make $ sudo make install 你也可以尝试用 LLVM 编译器而不是 GNU 编译器集合（GCC）进行编译。现在，我把这个问题留给你自己去研究（提示：尝试设置 CC 环境变量）。一旦你完成了对 Angband 的源代码和至少几个地牢的探索（你已经赢得了一些休息时间），可以看看其他一些代码库。很多人都会使用 Autotools 或 Cmake，而其他人可能会使用不同的东西。看看你能构建的成果！via: https://opensource.com/article/21/11/compiling-code作者：Seth Kenlon 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出