GCC 内建函数汇编展开详解

1. 引言

GNU 编译器集合(GCC)是广泛使用的开源编译器套件,支持多种编程语言,其中 C 语言编译器是其核心组件之一。在 C 语言编译过程中,GCC 不仅处理用户编写的标准 C 代码,还提供了一类特殊的函数------内建函数(Built-in Functions)。这些函数以 __builtin_ 前缀或与标准库函数同名的形式存在,它们并非由用户定义,而是由编译器内部直接支持。理解 GCC 内建函数如何在编译流程中被处理、优化并最终转换为特定目标架构的汇编代码,对于深入掌握 GCC 的工作原理、进行底层性能优化以及开发编译器相关工具至关重要。本报告旨在详细阐述 GCC 内建函数从 C 源码调用到最终生成汇编指令的完整生命周期,涵盖其定义、目的、在编译各阶段的处理、与优化的交互、中间表示(IR)转换以及目标架构的影响,并通过实例和工具进行说明。

2. GCC 内建函数概述

  • A. 定义与目的

    GCC 提供了大量的内建函数,其设计目的主要是为了优化。这些函数由编译器直接识别和处理,使得编译器能够利用其对函数语义的深刻理解来生成更高效的代码,这通常是标准库函数调用无法比拟的。编译器知道内建函数的具体行为、副作用以及可能存在的简化或特殊实现方式。例如,某些内建函数可以直接映射到目标处理器的特定高效指令。

  • B. 与标准库函数的区别

    1. 实现方式: 标准库函数(如 printf, memcpy)通常存在于外部库(如 libc)中,有独立的函数入口点,其地址可以获取。编译器通常只知道它们的函数签名(原型),并在链接阶段解析其地址。而 GCC 内建函数(除少数例外或特定优化场景外)通常由编译器在编译时直接处理,通常以内联展开(inline expansion)的方式实现,即用函数对应的代码序列替换调用点。因此,大多数纯粹的内建函数没有对应的函数入口点,也无法获取其地址。尝试获取它们的地址会导致编译时错误。
    2. __builtin_ 前缀: 许多标准 C 库函数都有对应的 GCC 内建版本,这些内建版本有两种形式:一种带有 __builtin_ 前缀(如 __builtin_memcpy),另一种则没有前缀(如 memcpy)。即使使用了 -fno-builtin 选项(该选项通常禁止 GCC 将标准库函数名识别为内建函数),带有 __builtin_ 前缀的版本仍然会被编译器识别为内建函数并进行特殊处理。不带前缀的版本默认也会被当作内建函数处理,除非显式使用 -fno-builtin 或针对特定函数使用 -fno-builtin-function
    3. 优化交互: 编译器对内建函数拥有完全的语义理解,这使得它们能更深入地参与优化过程。例如,如果 __builtin_memcpy 的长度参数是编译时常量,GCC 可以生成高度优化的、特定长度的拷贝代码序列,甚至可能完全消除拷贝(如果后续代码未使用目标内存)。对于标准库函数调用,编译器通常只能进行有限的优化,因为它视其为一个"黑盒"调用。
    4. libgcc 的角色: 某些情况下,即使是内建函数,编译器也可能决定不进行内联展开,或者内建函数的实现需要一些底层硬件不支持的操作(例如 32 位平台上的 64 位整数运算,或某些浮点运算)。在这种情况下,编译器会生成对 libgcc 库中相应辅助函数的调用。libgcc 是 GCC 的底层运行时库,提供了目标处理器可能无法直接执行的算术运算、异常处理等支持。
  • C. 内建函数的种类

    GCC 提供的内建函数种类繁多,大致可分为:

    1. 标准库函数对应版本:__builtin_memcpy, __builtin_memset, __builtin_printf, __builtin_abs, __builtin_sqrt, __builtin_sin 等,涵盖 C90, C99 及后续标准的许多函数。
    2. 优化与代码生成辅助:__builtin_expect(用于分支预测提示), __builtin_prefetch(用于数据预取), __builtin_constant_p(判断表达式是否为编译时常量), __builtin_types_compatible_p(判断类型兼容性)。
    3. 目标特定指令接口: 如用于 x86 的 SSE/AVX 指令、ARM 的 NEON 指令的内建函数,以及特定功能指令如 __builtin_popcount(计算置位比特数), __builtin_clz(计算前导零个数), __builtin_ctz(计算尾随零个数)。
    4. 原子操作:__sync_fetch_and_add, __atomic_load_n 等,提供跨平台原子操作接口。
    5. 其他:__builtin_alloca(栈上动态分配内存), __builtin_trap(生成陷阱指令), __builtin_speculation_safe_value(用于缓解推测执行攻击)。
  • D. 使用场景与覆盖

    内建函数主要用于性能敏感的代码,或者需要直接利用特定硬件特性的场景。通过使用内建函数,开发者可以向编译器提供更多信息,使其能够生成最优化的代码。在 freestanding 环境(如操作系统内核开发)中,标准库不可用,此时若想利用某些标准库函数的优化版本,可以通过宏定义将标准函数名映射到对应的 _builtin 版本(前提是编译器支持且决定进行优化,否则仍需自行实现或链接 libgcc 中的某些函数,如 memcpy, memset 等)。

  • E. 深层含义与影响

    内建函数的处理方式揭示了编译器设计中的一个重要权衡:灵活性与可预测性。GCC 对内建函数的处理并非一成不变,而是根据优化级别、上下文信息(如参数是否为常量)以及目标平台特性动态决定的。这种动态性使得编译器能够最大程度地利用可用信息进行优化。例如,一个 __builtin_memcpy 调用,在 -O0 下可能直接变成对库函数 memcpy 的调用,而在 -O3 且长度为已知小常量时,则可能被完全展开为几条 mov 指令。这种灵活性源于编译器在优化过程中做出的决策,而不是一个固定的转换规则。libgcc 的存在进一步证明了这种动态性,它作为内建函数无法或不适合内联展开时的后备机制。

    此外,内建函数不仅仅是被优化的对象,它们本身就是优化过程的基础元素 。它们向优化器暴露了函数的内部语义,这是普通库函数调用所不具备的。例如,__builtin_constant_p 直接参与到常量折叠过程中,而 __builtin_popcount 则让编译器有机会直接选用硬件的 popcntcnt 指令。这种语义透明性是内建函数能够带来显著性能提升的关键原因,它们为优化器提供了进行高级转换(如指令选择、常量求值)所需的"钩子"和信息。

3. GCC 编译流程概述

  • A. 编译阶段总览

    使用 GCC 编译 C 程序通常涉及四个主要阶段,这些阶段由 gcc 驱动程序按顺序调用相应的工具完成:

    1. 预处理 (Preprocessing): 此阶段由预处理器(通常集成在编译器主程序 cc1 中,但也可作为独立步骤 -E 调用 cpp)执行。它处理源代码文件(.c)中的预处理指令,如 #include(展开头文件)、#define(宏展开)、#if/#endif(条件编译),并移除注释。输出是一个经过预处理的 C 源代码文件,通常带有 .i 扩展名。
    2. 编译 (Compilation): 这是核心阶段,由编译器本身(如 cc1 for C)执行。它接收预处理后的 .i 文件,进行词法分析、语法分析、语义分析,生成中间表示(如 GIMPLE, RTL),执行大量的优化,并最终生成特定于目标架构的汇编代码。输出是汇编语言文件,通常带有 .s 扩展名。
    3. 汇编 (Assembly): 此阶段由汇编器(如 GNU Assembler as)执行。它将编译阶段生成的汇编代码(.s 文件)翻译成机器语言指令,并将结果打包成可重定位的目标文件(object file)。目标文件包含了代码段、数据段以及符号表等信息,通常带有 .o 扩展名。
    4. 链接 (Linking): 此阶段由链接器(如 GNU Linker ld,通常通过 collect2 调用)执行。它将一个或多个目标文件(.o 文件)以及所需的库文件(静态库 .a 或动态库 .so)组合起来,解析外部符号引用(如函数调用、全局变量访问),分配最终的内存地址,并生成最终的可执行文件(或共享库)。默认可执行文件名为 a.out
  • B. 内建函数处理的关键阶段

    GCC 内建函数的识别、转换和优化主要发生在编译 (Compilation) 阶段。正是在这个阶段,编译器将 C 源代码(包括内建函数调用)转换为内部的中间表示(IR),并在此 IR 上执行各种分析和转换。内建函数的特殊语义在这个阶段被编译器利用,以进行内联展开、常量折叠、指令选择等优化操作。虽然链接阶段会处理对外部库函数(包括 libgcc 中可能由内建函数回退调用的函数)的引用解析,但内建函数调用本身的转换逻辑是在编译阶段完成的。

  • C. 深层含义与影响

    将内建函数的处理集中在编译阶段,这突显了它们作为编译器内在构造的本质。它们不同于预处理器宏(在预处理阶段被文本替换掉),也不同于外部库函数(其符号在链接阶段才被解析)。内建函数的转换与编译器核心的优化引擎和代码生成器紧密耦合,这些引擎操作于 GIMPLE 和 RTL 等中间表示之上。这意味着对内建函数的处理能够充分利用编译器在编译阶段积累的关于代码结构、数据流和控制流的丰富信息,从而实现比处理外部函数调用更深层次的优化。这种设计选择使得内建函数成为沟通高级语言语义与底层硬件能力之间的有效桥梁,其转换逻辑直接受益于编译器的全局视角和优化能力。

4. 初始转换:从 C 到 GIMPLE

  • A. GCC 中间表示 (IR) 的必要性

    像 GCC 这样需要支持多种源语言(C, C++, Fortran 等)和多种目标硬件架构(x86, ARM, RISC-V 等)的编译器,采用中间表示(Intermediate Representation, IR)是关键的设计策略。IR 提供了一个抽象层:不同的前端(front ends)负责将各自的源语言解析成一种通用的高级 IR,而不同的后端(back ends)则负责将一种通用的低级 IR 翻译成特定目标的机器代码。这大大减少了需要实现的转换路径数量(从 M 种语言 * N 种目标 到 M 个前端 + N 个后端)。

  • B. GENERIC 与 GIMPLE

    1. GENERIC: GCC 使用一种名为 GENERIC 的高级 IR,它本质上是一种语言无关的抽象语法树(Abstract Syntax Tree, AST)。不同语言的前端将源代码解析后生成对应的 GENERIC 树。
    2. GIMPLE: 为了便于进行优化,GENERIC 树会被降低(lower)为一种更简单的、基于三地址码(three-address code)的 IR,称为 GIMPLE。GIMPLE 的设计受到了 McGill 大学的 SIMPLE IL 的影响。其核心思想是将复杂的表达式分解为一系列最多包含三个操作数(如 result = operand1 op operand2)的元组(tuples),并引入临时变量来存储中间结果。同时,复杂的控制流结构(如 if, while, for)被转换为显式的条件跳转和标签。GIMPLE 有两个主要形式:High GIMPLE 保留了一些结构化信息(如词法作用域 GIMPLE_BIND),而 Low GIMPLE 则完全展平了控制流,更接近传统的控制流图(CFG)。将 GENERIC 转换为 GIMPLE 的过程被称为 "gimplification",由 "gimplifier" 完成。值得注意的是,C 和 C++ 的前端目前通常直接将解析树转换为 GIMPLE,而不是先生成 GENERIC。
  • C. GIMPLE 中内建函数的表示

    当 C 代码中的内建函数调用(例如 y = __builtin_abs(x);)被处理时,在 GIMPLE 层面,它很可能最初被表示为一个特定的 GIMPLE_CALL 语句或类似的节点结构。虽然关于内建函数在 GIMPLE 中具体表示的公开文档有限,但这种表示必须能够清晰地标识出这是一个内建函数调用(可能通过函数名 __builtin_abs 或内部标志),并包含其参数信息。这使得后续的 GIMPLE 优化遍(passes)能够识别出这个调用,并根据其已知的特殊语义进行处理。gimplifier 在转换过程中扮演了关键角色,它负责将前端的表示(无论是 AST 还是 GENERIC)转换成 GIMPLE 形式,可能还需要语言特定的钩子函数 LANG_HOOKS_GIMPLIFY_EXPR 来处理非标准的语言结构。

  • D. 深层含义与影响

    GIMPLE 作为 GCC 中第一个进行大规模、语言无关优化的 IR,其对内建函数的表示方式至关重要。正是在 GIMPLE 层面,编译器开始利用其对内建函数语义的了解。如果 __builtin_constant_p(10) 在 GIMPLE 中被表示出来,那么像常量传播这样的优化遍就能识别它,并在 GIMPLE IR 上直接求值,可能消除相关的条件分支。因此,GIMPLE 不仅是代码结构化的表示,更是优化开始的竞技场。内建函数在 GIMPLE 中的表示,决定了它们如何与这些早期的、强大的优化过程互动,是后续一系列转换的基础。

    关于 GIMPLE 中内建函数确切表示的文档缺乏,可能暗示这被视为编译器的内部实现细节。一种可能的实现方式是,将内建函数调用初步表示为标准的 GIMPLE_CALL,但附加特殊的标记或属性,或者仅仅依赖于其独特的 __builtin_ 名称来被后续的优化遍识别。对于编译器设计而言,优化遍如何行为(即如何识别和转换这些调用)通常比它们在 IR 中具体使用哪个数据结构名称更为重要。这种方式复用了现有的 IR 结构,是一种务实且高效的实现策略。

5. Tree-SSA 优化与内建函数

  • A. Tree SSA 形式

    GCC 在 GIMPLE IR 上广泛使用静态单赋值(Static Single Assignment, SSA)形式进行优化。在 SSA 形式下,每个变量在其生命周期内只被赋值一次。如果一个变量在原始代码中被多次赋值,那么在 SSA 形式中会创建该变量的不同版本(通常通过下标区分,如 x_1, x_2)。当不同的控制流路径汇合时(例如 if 语句之后或循环头部),需要合并来自不同路径的变量值,这时会引入特殊的 PHI 函数(Φ 函数)。例如,x_3 = PHI <x_1(bb2), x_2(bb3)> 表示在当前基本块(basic block)的入口处,x_3 的值可能是来自基本块 bb2 的 x_1,也可能是来自基本块 bb3 的 x_2。SSA 形式极大地简化了许多数据流分析和优化算法的实现,如常量传播、死代码消除等。

  • B. 内建函数与优化遍的交互

    在 Tree-SSA (GIMPLE) 层面,内建函数与各种优化遍发生密切的交互:

    1. 常量折叠与传播 (Constant Folding/Propagation): 这是内建函数发挥重要作用的领域。如果一个内建函数的参数是编译时常量,并且该内建函数本身可以在编译时求值,那么编译器(在如 cpropfold 等遍中)可以直接计算出结果,用常量替换掉整个函数调用。例如,__builtin_constant_p(10) 会被直接判定为真(返回 1)。类似地,__builtin_clz(0x1000)(计算常量 0x1000 的前导零个数)也可能在编译时直接计算出结果。这个常量结果随后可以通过常量传播影响后续代码的优化。
    2. 内联 (Inlining): 对于语义相对简单的内建函数,如 __builtin_abs 或某些简单的数学函数,编译器在 inline 遍中可能会选择将其 GIMPLE 实现直接嵌入到调用点。这避免了函数调用的开销。对于更复杂的内建函数,或者当优化策略(如 -O0 或代码大小优先 -Os)不倾向于内联时,它们可能仍然保留为 GIMPLE 调用。
    3. 简化 (Simplification): 编译器利用其对内建函数数学或逻辑属性的了解来进行代数简化。例如,在 simplify 遍中,__builtin_sqrt(x*x) 可能会被简化为等价的 __builtin_fabs(x)(假设 x 是浮点数)。
    4. 特定模式优化: 某些内建函数调用模式可以触发特殊的优化。一个经典的例子是 printf("constant string\n")。编译器知道 printf 的语义,当格式化字符串是常量且不包含格式说明符,并且以换行符结尾时,它可以安全地将这个调用优化为更高效的 puts("constant string") 调用。类似地,__builtin_speculation_safe_value 这类内建函数的设计目的就是为了与编译器针对推测执行漏洞的优化策略协同工作。
  • C. 优化级别 (-O) 的影响

    GCC 提供的优化级别(如 -O0, -O1, -O2, -O3, -Os, -Oz)直接控制了哪些优化遍会被执行以及它们的积极程度。-O0 表示基本不进行优化,内建函数可能大多保留为调用形式(或回退到 libgcc 调用)。随着优化级别的提高(-O1, -O2, -O3),越来越多的 Tree-SSA 优化遍会被启用,并且它们的优化力度会加大。例如,更复杂的内联、更彻底的常量传播、循环优化等会在 -O2 或 -O3 时发生。-Os 和 -Oz 则在启用大部分 -O2 优化的同时,会避免那些倾向于显著增加代码体积的优化。因此,同一个包含内建函数的 C 源码,在不同优化级别下编译,其在 GIMPLE 阶段经历的转换和最终形态可能会大相径庭。

  • D. 深层含义与影响

    内建函数与 Tree-SSA 优化器之间存在一种共生关系。一方面,内建函数向优化器提供了精确的语义信息,这是优化得以进行的前提。例如,没有 __builtin_constant_p 提供的"是否为常量"信息,常量折叠就无法安全地应用于依赖此判断的代码。另一方面,优化器作用于包含内建函数的代码,对其进行转换、简化甚至完全消除。__builtin_clz(constant) 可能被优化器直接求值替换,而 printf 调用则可能被优化器根据其参数替换为 puts。这种双向互动是 GCC 实现高性能编译的关键机制之一。

    同时,这也凸显了优化级别作为关键决定因素 的重要性。开发者选择的 -O 级别直接决定了作用于内建函数之上的优化流水线的构成和强度。一个内建函数调用在 -O3 下可能被彻底优化掉,而在 -O1 下可能只是简单内联,在 -O0 下则可能保持为对 libgcc 的调用。这意味着理解特定内建函数在给定场景下的行为,必须结合考虑所使用的优化级别。

6. 降低到机器层面:从 GIMPLE 到 RTL

  • A. 寄存器传输语言 (RTL) 简介

    在 GIMPLE 和 Tree-SSA 优化之后,GCC 将代码的表示从 GIMPLE 降低(lower)到一种更接近机器指令的低级中间表示,称为寄存器传输语言(Register Transfer Language, RTL)。RTL 的抽象层次低于 GIMPLE,它描述了数据如何在寄存器、内存和常量之间传输和运算,其形式更接近于汇编语言。RTL 在 GCC 内部以 C 结构体表示,但在调试输出(dump 文件)中通常使用一种类似 Lisp 的文本语法,通过嵌套括号来表示内部结构指针。RTL 的基本构成元素包括:表达式(RTX, Register Transfer eXpression),如 (reg:M n) 表示访问机器模式为 M 的寄存器 n,(mem:M addr) 表示访问内存地址 addr;指令(insn),代表一个或多个操作;机器模式(machine modes),指定操作数的大小和类型(如 SI 代表 32 位整数,DI 代表 64 位整数);以及其他如寄存器、内存引用、常量等对象。

  • B. GIMPLE 到 RTL 的转换过程

    从 GIMPLE 到 RTL 的转换是编译流程中的一个关键步骤,由 GCC 的"扩展"(expand)阶段完成。在此阶段,每个 GIMPLE 语句被翻译成一个或多个 RTL 指令(insn)序列。对于在 GIMPLE 阶段仍然存在的内建函数调用,其转换方式有以下几种可能:

    1. 直接 RTL 展开: 对于一些足够简单的内建函数,编译器内部可能包含直接生成对应 RTL 指令序列的逻辑。例如,一个简单的算术内建函数可能被直接转换为几个 RTL 算术和移动操作。
    2. 映射到命名模式 (Named Patterns): 许多内建函数(尤其是那些对应标准操作的,如整数乘法、加法等)会被降低为 GCC 内部预定义的、具有特定名称的 RTL 模式(pattern)。例如,一个 32 位整数乘法操作(可能源自 __builtin_mulsi3 或普通乘法运算符)会被表示为 (mult:SI...) 形式,并可能包含在一个名为 mulsi3insn 模式中。这些命名模式是后续基于机器描述文件进行指令选择的基础。
    3. 生成库调用: 如果一个内建函数在 GIMPLE 层面未被优化掉或内联,并且没有直接的 RTL 展开逻辑或映射到标准模式,编译器可能会生成一个 RTL 的 call_insn。这个调用指令的目标通常是 libgcc 库中对应的辅助函数(如 __popcountsi2 对应 __builtin_popcount)或者是标准 C 库中的函数(如果内建函数是标准库函数的一个优化接口,且优化未发生)。

    需要注意的是,关于每个内建函数具体如何精确地表示为 RTL 的公开文档同样有限。实际的处理方式通常是在 expand 遍中,通过编译器内部的模式匹配逻辑或特定函数处理代码来完成。

  • C. 深层含义与影响

    RTL 作为连接 GIMPLE 和最终汇编代码的桥梁,其生成过程是抽象操作向具体硬件能力映射的开始。在 GIMPLE 层面,操作(包括内建函数)相对抽象且独立于目标机器。但在 GIMPLE 到 RTL 的转换过程中,编译器的目标知识开始发挥作用。选择将一个内建函数展开为内联 RTL 序列、映射到一个命名模式,还是生成一个库调用,这个决策直接影响了最终代码的结构和性能潜力。例如,映射到命名模式 mulsi3 意味着后续可以利用机器描述文件中定义的最高效的乘法指令;而生成对 libgcc 的调用则意味着函数调用开销和对运行时库的依赖。

    虽然内建函数的 __builtin_ 名称在 RTL 层面可能不再直接可见(特别是当它被展开为指令序列或命名模式时),但其原始语义信息以某种形式得以保留 。例如,mulsi3 这个模式名称本身就携带了"32 位整数乘法"的语义。这种隐式的身份保持对于后续的 RTL 优化遍和最终的指令选择至关重要。只有当 RTL 准确反映了原始操作的意图时,后续阶段才能正确地对其进行优化和转换,确保例如 __builtin_popcount 最终能被映射到硬件 popcnt 指令(如果可用且合适)。

7. RTL 优化与内建函数

  • A. RTL 优化遍

    在代码表示为 RTL 之后,GCC 会执行一系列针对性的优化遍(passes)来进一步改进代码,使其更接近最优的机器指令序列。这些优化遍工作在比 GIMPLE 更低的抽象层次上,能够处理与寄存器、内存访问和指令序列相关的细节。一些重要的 RTL 优化遍包括:

    • 公共子表达式消除 (CSE): 包括局部 CSE (cse1, cse2) 和全局 CSE (gcse1, gcse2),用于消除基本块内部或跨基本块的冗余计算。
    • 跳转优化 (Jump Optimization):jump 遍,用于简化控制流,例如消除跳转到下一条指令的跳转、跳转到跳转的跳转等。
    • 指令合并 (Instruction Combination): combine 遍尝试将多个 RTL 指令合并成一个更有效的指令(如果目标架构支持)。
    • 窥孔优化 (Peephole Optimization): peephole2 遍检查指令序列中的小窗口,寻找可以用更短或更快的指令序列替换的模式。
    • 指令调度 (Instruction Scheduling): sched1, sched2 等遍根据目标处理器的流水线特性和指令延迟,重新排列指令顺序以减少等待时间,提高执行效率。
    • 寄存器分配 (Register Allocation): ira (Iterated Register Allocation) 遍将 RTL 中使用的无限虚拟寄存器映射到有限的目标机器物理寄存器上。
    • 死代码消除 (Dead Code Elimination): dce 遍移除计算结果从未被使用的指令。
  • B. RTL 优化对内建函数代码的影响

    这些 RTL 优化遍同样会作用于由内建函数调用转换而来的 RTL 代码序列:

    1. 指令合并与简化: combinecse 遍可能会发现由内建函数展开产生的 RTL 序列中存在冗余或可以合并的操作。例如,如果一个内建函数的结果被立即用于另一个操作,combine 可能会尝试将这两个操作合并成一条复合指令(如带偏移量的加载/存储)。
    2. 为指令选择做准备: 虽然最终的指令选择依赖于机器描述文件,但 RTL 优化遍可能会将指令序列转换成某种"规范形式",这种形式更容易被机器描述文件中的高效指令模式所匹配。
    3. 死代码消除: 如果内建函数调用的结果(经过 GIMPLE 优化后)在后续代码中实际上没有被使用,那么对应的 RTL 指令序列可能在 RTL 阶段的 dce 遍中被完全移除。
    4. 寄存器分配: ira 遍负责为保存内建函数参数和结果的虚拟寄存器分配物理寄存器。分配的好坏直接影响最终代码性能,特别是当内建函数涉及多个操作数时。
  • C. 深层含义与影响

    RTL 优化提供了在生成最终汇编代码之前的最后一次精细调整机会。由于 RTL 更接近机器层面,这些优化可以考虑到 GIMPLE 层面无法完全表达或处理的机器相关细节(如指令延迟、寄存器压力)。因此,RTL 优化能够捕捉到 GIMPLE 优化遗漏的机会,进一步改善由内建函数(以及其他代码)生成的指令序列的质量。

    RTL 优化与目标机器描述文件之间存在紧密的协同关系 。优化遍(如 combine)的目标不仅仅是减少指令数量或消除冗余,它们也可能旨在将 RTL 转换成更容易被机器描述文件(.md 文件)中高效指令模式匹配的形式。这种协同确保了优化后的 RTL 能够有效地利用目标硬件的最佳指令,使得从高级内建函数到底层高效汇编的转换路径更加顺畅。

8. 生成汇编:从 RTL 到目标代码

  • A. 机器描述文件 (.md 文件) 的角色

    GCC 实现跨平台编译的核心在于其后端使用了目标特定的机器描述(Machine Description)文件,通常命名为 target.md(如 i386.md, arm.md)。这些文件是 GCC 后端的"知识库",它们用一种特殊的语言(基于 RTL 和 Lisp 风格的宏)定义了目标处理器的几乎所有特性,包括:

    • 指令集体系结构(ISA):定义了可用的汇编指令。
    • 寄存器:定义了寄存器的数量、类型(通用、浮点、向量等)和名称。
    • 寻址模式:定义了合法的内存访问方式。
    • 指令到 RTL 的映射:最关键的是,它们定义了如何将编译器内部的 RTL 指令模式(patterns)翻译成具体的汇编指令字符串。
  • B. 指令模式 (define_insn)

    .md 文件中定义指令映射的主要方式是使用 define_insn 宏。每个 define_insn 描述了一个或一组相关的汇编指令,并指定了它对应的 RTL 模式。其结构通常包含以下部分:

    1. 名称 (Name): 一个内部使用的字符串名称(可选,但通常有,如 "mulsi3"),用于调试或由编译器内部代码引用。
    2. RTL 模板 (RTL Template): 一个 RTL 表达式,描述了该指令模式所匹配的操作和操作数结构。例如,`` 匹配一个将操作数 1 和 2 的 32 位整数乘积存入操作数 0 的操作。
    3. 操作数约束 (Operands with Predicates and Constraints): 使用 match_operand 来定义每个操作数。每个 match_operand 包含:
      • 机器模式 (Machine Mode): 如 :SI
      • 操作数编号 (Operand Number): 从 0 开始。
      • 谓词 (Predicate): 一个字符串(如 "register_operand", "immediate_operand"),定义在 predicates.md 中,用于初步检查操作数是否符合基本类型要求(如必须是寄存器)。
      • 约束 (Constraint): 一个更具体的字符串(如 "r" 表示通用寄存器, "m" 表示内存操作数, "i" 表示立即数),定义在 constraints.md 中。约束不仅用于匹配,还指导寄存器分配器确保操作数位于指令要求的正确位置(如特定类型的寄存器)。
    4. 条件 (Condition): 一个可选的 C++ 表达式字符串。如果该表达式在编译时求值为假,则此 define_insn 模式将被禁用。这常用于处理同一架构的不同变体或可选特性(例如,某个指令只在支持特定扩展的 CPU 上可用)。
    5. 输出模板 (Output Template): 一个字符串,包含了要生成的汇编指令的字面文本。其中 %0, %1, %n 等占位符将被替换为匹配到的实际操作数(寄存器名、内存地址、立即数等)。输出模板可以包含多行(用 \n 分隔)或使用 @ 分隔的备选模板,编译器会根据匹配到的操作数约束选择合适的模板。也可以包含 C++ 代码片段(用 {} 包裹)来动态生成汇编字符串。
  • C. 匹配与发射过程

    GCC 的最终代码生成阶段(通常在所有 RTL 优化之后)会遍历函数中的 RTL 指令(insn)流。对于每个 insn(或有时是一个 insn 序列),编译器会在目标机器的 .md 文件中搜索所有 define_insn 模式。它寻找满足以下条件的第一个模式:

    1. 该模式的 RTL 模板与当前的 RTL insn 结构匹配。
    2. insn 中的每个操作数都满足模式中对应 match_operand 的谓词和约束。
    3. 模式的条件表达式(如果有)求值为真。

    一旦找到一个成功的匹配,编译器就认为这个 define_insn 是实现该 RTL 操作的最佳方式。然后,它将 RTL insn 中的实际操作数(已经被寄存器分配器分配了物理寄存器或确定了内存地址/立即数)代入到匹配模式的输出模板中,替换掉 %0, %1 等占位符,从而生成最终的汇编指令字符串。这个字符串随后被写入到 .s 输出文件中。

    这个过程将内建函数(经过 GIMPLE 和 RTL 优化后留下的 RTL 表示)最终转换为一条或多条具体的、目标架构相关的汇编指令。例如,如果 __builtin_popcount 被降低为某个特定的 RTL 模式,并且目标 .md 文件中有一个 define_insn 将该模式映射到硬件 popcnt 指令,那么最终就会生成 popcnt 汇编指令。

  • D. 深层含义与影响

    机器描述文件(.md)是连接编译器内部世界 (RTL) 与外部物理世界(目标处理器汇编)的最终、权威的桥梁。.md 文件的质量------其模式的覆盖度、精确性和对目标指令的优化利用程度------直接决定了编译器将 RTL(包括源自内建函数的 RTL)翻译成汇编代码的效率。一个设计良好、维护更新及时的 .md 文件是 GCC 能够为特定目标生成高性能代码的关键。如果一个内建函数被优化并降低为一个高效的 RTL 模式,但 .md 文件中没有为其定义一个映射到最佳硬件指令的 define_insn,那么编译器的优化成果就可能在最后一步丢失。

    此外,define_insn 中的操作数约束不仅仅用于验证匹配,它们还反向驱动了之前的寄存器分配过程 。寄存器分配器(如 ira 遍)需要参考 .md 文件中的约束信息,来确保在分配物理寄存器时,满足后续指令选择阶段可能选用的指令对操作数位置(如必须在某个特定类型的寄存器中)的要求。这种前后阶段的信息交流确保了生成的 RTL 和最终选择的汇编指令能够正确、高效地协同工作。

9. 目标架构的影响

  • A. 架构相关的代码生成

    对于给定的 C 源代码,尤其是包含旨在利用特定硬件功能的内建函数的代码,最终生成的汇编指令高度依赖于目标处理器架构(例如 x86-64, ARMv7, AArch64, RISC-V 等)。这是因为不同架构拥有不同的指令集、寄存器配置、寻址模式和性能特性,这些都在相应的 .md 文件中有所体现,并直接影响从 RTL 到汇编的转换过程。

  • B. 案例研究:__builtin_popcount

    __builtin_popcount 函数用于计算一个整数中置位(值为 1)的比特数量,是展示架构影响的一个绝佳例子:

    1. x86-64 架构:
      • 硬件指令: 如果目标 CPU 支持 POPCNT 指令(属于 SSE4.2 或 AMD 的 ABM 扩展集的一部分),并且编译时通过 -mpopcnt 或包含此特性的 -march=native 等选项告知了 GCC,那么 GCC 通常会将 __builtin_popcount 直接翻译成一条 popcnt 汇编指令。这条指令在硬件层面直接完成计数,效率很高,尽管其延迟可能不止一个周期。
      • 软件实现/库调用: 如果目标 CPU 不支持 POPCNT 指令,或者编译时未启用该特性,GCC 则会采取后备策略。它可能会生成一段使用其他位操作指令(如移位、与、加法)实现的软件计数算法,或者生成一个对 libgcc 库中名为 __popcountsi2(用于 32 位整数)或 __popcountdi2(用于 64 位整数)的辅助函数的调用。这些后备方案的性能通常远低于硬件 popcnt 指令。
    2. ARM/AArch64 架构:
      • 硬件指令: 在现代 ARM 架构中,特别是 AArch64(ARM 64位),通常存在专门的计数指令。例如,AArch64 提供了 CNT 指令,而 ARM 的 NEON SIMD 扩展中则有 VCNT 指令可以用于向量化的种群计数。如果目标 ARM 处理器支持这些指令,并且 GCC 的 .md 文件配置正确,__builtin_popcount 就可能被映射到这些高效的硬件指令。一个重要的区别是,CNT 指令在 AArch64 架构中通常是基础指令集的一部分,不像 x86 的 POPCNT 那样属于扩展特性,因此在 AArch64 上使用硬件指令的可能性更高。
      • 软件实现/库调用: 对于不支持专用计数指令的旧版 ARM 处理器,或者在特定编译配置下,GCC 同样会回退到软件实现的算法或调用 libgcc 中的相应函数。
  • C. 其他架构影响示例

    除了 popcount,许多其他内建函数也体现了架构差异:

    • SIMD (单指令多数据流) 操作: 用于向量计算的内建函数(如对 packed data 进行加法、乘法)会映射到目标架构的 SIMD 指令集,如 x86 上的 SSE, AVX, AVX-512,或 ARM 上的 NEON。不同 SIMD 架构的指令、寄存器宽度和能力差异巨大。
    • 原子操作: __sync_*__atomic_* 系列内建函数用于实现线程安全的原子操作。它们在不同架构上会映射到不同的原子原语。例如,在 x86 上可能使用带 lock 前缀的指令(如 lock xadd),而在 ARM 上则可能使用 Load-Linked/Store-Conditional (LL/SC) 指令对(如 ldrex/strexldaxr/stlxr)。
    • 特定目标内建函数: GCC 还提供了一些名称中就明确包含目标架构的内建函数,如 __builtin_alpha_* 系列用于 Alpha 架构,或 __builtin_arm_* 系列用于 ARM。这些函数直接暴露了该架构独有的特性或指令。
  • D. 深层含义与影响

    内建函数提供了一种在源代码层面保持可移植性的方式来请求特定的、通常与硬件相关的操作(如 popcount)。开发者可以使用相同的 __builtin_popcount 调用来编写代码,而编译器则负责将这个抽象请求映射到当前目标架构的最佳实现。这个映射可能是直接使用硬件指令,也可能是调用 libgcc 函数,或者是内联一段软件算法。编译器后端和 .md 文件构成了这个抽象层,隐藏了底层的实现细节。

    然而,这种抽象并非没有代价。虽然源代码可移植,但实际性能表现可能因目标架构而异 。更重要的是,为了让编译器能够生成最优代码(即使用硬件指令而非慢速后备方案),开发者通常需要显式地告知编译器目标处理器的具体型号或特性集 。这通过使用 -march=, -mcpu=, -mtune= 等编译选项来实现。如果省略这些选项,GCC 可能会为了保证代码能在更广泛的同系列处理器上运行,而保守地假设只存在基线指令集,从而无法利用 POPCNTCNT 等高级指令,导致内建函数的性能优势无法体现。因此,正确配置目标架构选项对于发挥内建函数的全部潜力至关重要。

10. 观察转换过程:实用工具

  • A. 生成最终汇编 (-S)

    获取内建函数最终转换结果的最直接方法是使用 -S 编译选项。该选项指示 GCC 在完成编译阶段(包括所有优化和汇编代码生成)后停止,而不是继续进行汇编和链接。输出是一个以 .s 为扩展名的人类可读的汇编代码文件。通过检查这个文件,可以直接看到内建函数调用最终被转换成了哪些具体的机器指令,这对于特定的目标架构和优化级别是最终的"真相"。

  • B. 转储中间表示 (-fdump-*)

    为了深入理解内建函数在编译过程中经历的转换,GCC 提供了一系列强大的 -fdump-* 调试选项。这些选项用于在编译流程的不同阶段将编译器内部的中间表示(IR)转储(dump)到文件中,供开发者检查。

    • -fdump-tree-* 系列: 用于转储 GIMPLE(Tree SSA)表示。例如,-fdump-tree-gimple 转储初始 GIMPLE 形式,-fdump-tree-optimized 转储 GIMPLE 优化后的结果。-fdump-tree-all 会转储所有 Tree 优化遍的输出。
    • -fdump-rtl-* 系列: 用于转储 RTL 表示。例如,-fdump-rtl-expand 转储从 GIMPLE 转换来的初始 RTL,-fdump-rtl-combine 转储指令合并后的 RTL,-fdump-rtl-final 转储接近最终汇编的 RTL。-fdump-rtl-all 会转储所有 RTL 优化遍的输出。
    • -fdump-ipa-* 系列: 用于转储过程间分析(Interprocedural Analysis)相关的信息,如调用图、内联决策等。
    • 这些转储选项通常会生成大量文件,文件名基于源文件名、遍编号和遍名称(例如 your_code.c.038t.optimizedyour_code.c.110r.final)。
  • C. 追踪内建函数的关键转储选项

    要追踪一个内建函数从源代码到汇编的完整生命周期,以下几个 -fdump-* 选项特别有用:

    1. -fdump-tree-gimple-fdump-tree-original: 查看内建函数调用在最初进入 GIMPLE IR 时的表示。
    2. -fdump-tree-optimized: 查看在所有 GIMPLE 层面优化(如常量折叠、简化)完成后,内建函数调用变成了什么形式。
    3. -fdump-tree-inline: 检查内建函数是否在 GIMPLE 层面被内联。
    4. -fdump-rtl-expand: 这是观察 GIMPLE 到 RTL 转换的关键点。查看内建函数是被展开为 RTL 指令序列,还是被转换为对 libgcc 或库函数的调用。
    5. -fdump-rtl-combine / -fdump-rtl-cse: 观察 RTL 优化如何进一步处理来自内建函数的代码。
    6. -fdump-rtl-final: 查看在寄存器分配、指令调度等几乎所有 RTL 优化完成后的最终 RTL 形式。这通常是与最终汇编代码最接近的 IR 表示。
    7. -fverbose-asm: 这个选项与 -S 结合使用,可以在生成的汇编代码中添加注释,将汇编指令与原始 C 源代码行以及可能的 RTL 指令关联起来,有助于理解汇编代码的来源。
    8. -fdump-passes: 列出当前编译选项下所有启用和禁用的优化遍,帮助理解编译流程和选择合适的 -fdump-tree-*-fdump-rtl-* 选项。
  • D. 解读转储文件

    需要注意的是,GIMPLE 和 RTL 的转储文件使用了 GCC 内部的表示语法,并且可能非常冗长。有效解读这些文件通常需要对 GCC 的内部工作原理、IR 结构以及各个优化遍的目标有一定的了解。查阅 GCC Internals 手册对于理解这些输出至关重要。

  • E. 表格:用于内建函数分析的有用 -fdump-* 标志

    下表总结了一些在分析 GCC 内建函数转换过程中特别有用的 -fdump-* 选项:

|-------------------------|--------------|----------------------------------|
| 标志 (Flag) | 阶段/IR | 对内建函数的关联性 |
| -fdump-tree-gimple | GIMPLE (早期) | 查看内建函数调用在 gimplification 后的初始表示。 |
| -fdump-tree-optimized | GIMPLE (优化后) | 查看 Tree-SSA 优化后的结果;显示常量折叠、简化的效果。 |
| -fdump-tree-inline | GIMPLE (遍) | 显示内建函数是否/如何在 GIMPLE 层面被内联。 |
| -fdump-rtl-expand | RTL (早期) | 初始 RTL 生成;关键在于观察是变成内联 RTL 还是库调用。 |
| -fdump-rtl-combine | RTL (遍) | 显示指令合并对源自内建函数的 RTL 的影响。 |
| -fdump-rtl-final | RTL (晚期) | 汇编生成前的 RTL 状态;反映了大多数优化和分配。 |
| -S | 汇编 | 针对目标架构生成的最终汇编代码。 |
| -fverbose-asm | 汇编 | 为 -S 输出添加注释,帮助关联源代码/IR。 |

  • F. 深层含义与影响 GCC 提供的众多转储选项既是其强大调试能力的体现,也反映了其内部编译过程的高度复杂性。有效利用这些工具需要投入时间学习 GCC 的内部结构、IR 语法和优化遍的知识,这使得编译器行为分析成为一项具有挑战性的任务。然而,对于需要深入理解特定代码段为何生成某种汇编、诊断性能问题或进行编译器开发的工程师来说,这些调试标志是不可或缺的窗口,它们揭示了从高级语言到机器代码转换过程中隐藏的复杂决策和转换。

11. 实例演练:追踪 __builtin_clz

  • A. 示例代码与选择

    我们选择 __builtin_clz (Count Leading Zeros) 作为示例,因为它相对简单,且其实现直接受到目标架构指令集的影响。以下是示例 C 代码 (builtin_example.c):
    C

    复制代码
    #include <stdio.h>
    
    // 计算无符号整数的前导零个数
    int count_leading_zeros(unsigned int x) {
      // 对于输入 0,__builtin_clz 的行为是未定义的,这里可以特殊处理
      if (x == 0) {
        return 32; // 假设是 32 位整数
      }
      // 调用内建函数
      return __builtin_clz(x);
    }
    
    int main() {
      unsigned int val = 0x000FFFFF; // 一个示例值
      int zeros = count_leading_zeros(val);
      printf("Value: 0x%x, Leading Zeros: %d\n", val, zeros); // 预期输出 8
      return 0;
    }
  • B. 编译命令 (x86-64 示例)

    我们使用以下命令在 x86-64 平台上编译,启用 -O2 优化,并请求目标处理器支持的特性(通过 -march=native,假设其包含 LZCNT 或 BMI1),同时生成汇编和几个关键的 IR 转储文件:
    Bash

    复制代码
    gcc -O2 -march=native -S \
        -fdump-tree-optimized \
        -fdump-rtl-expand \
        -fdump-rtl-final \
        builtin_example.c -o builtin_example
  • C. GIMPLE 分析 (.optimized 转储)

    检查生成的 builtin_example.c.*.optimized 文件中 count_leading_zeros 函数的部分。可能会看到类似以下的 GIMPLE (简化表示):
    代码段

    复制代码
    count_leading_zeros (unsigned int x)
    {
      int D.xxxxx; // 编译器生成的内部变量名
    
      if (x == 0)
        {
          D.xxxxx = 32;
          goto <L1>; // 跳转到返回语句
        }
      else
        {
          // _1 可能是一个临时变量
          _1 = __builtin_clz (x); // 内建函数调用仍然存在
          D.xxxxx = _1;
          goto <L1>;
        }
     <L1>:;
      return D.xxxxx;
    }

    在这个阶段,__builtin_clz 调用通常还存在,因为 GIMPLE 优化可能无法直接对其求值(除非 x 是常量)。

  • D. RTL 分析 (.expand.final 转储)

    1. .expand 转储: 检查 builtin_example.c.*.expand 文件。这里是 GIMPLE 到 RTL 的转换点。可能会看到 __builtin_clz(x) 被转换成了一个特定的 RTL 模式或指令。例如,它可能被转换成一个代表"count leading zeros"操作的内部 RTL 表达式,或者,如果编译器决定使用库调用,则会看到一个 (call_insn... (symbol_ref ("__clzsi2"))...)。假设 -march=native 使得 GCC 知道有硬件指令可用,那么更可能看到前者。
    2. .final 转储: 检查 builtin_example.c.*.final 文件。这是接近最终汇编的 RTL。经过了指令合并、调度、寄存器分配等优化。如果目标支持 LZCNTBSR 指令,这里的 RTL 应该直接反映了将要生成的指令。例如,可能会看到类似 (set (reg:SI Rdest) (clz:SI (reg:SI Rsrc))) 这样的 RTL 指令(clz 代表 count leading zeros 操作),其中 RdestRsrc 已经是分配好的物理寄存器。
  • E. 汇编分析 (.s 文件)

    打开生成的 builtin_example.s 文件,找到 count_leading_zeros 函数的汇编代码。在现代 x86-64 处理器上(假设 -march=native 识别到 BMI1 或更高版本),很可能会看到类似以下的指令序列(AT&T 语法):
    代码段

    复制代码
    count_leading_zeros:
        testl   %edi, %edi      # 检查 x 是否为 0 (x 在 %edi 寄存器)
        je    .L_zero_case    # 如果 x == 0 跳转
        lzcntl  %edi, %eax      # 使用 LZCNT 指令计算前导零,结果放入 %eax
        ret                     # 返回结果

.L_zero_case:

movl $32, %eax # 如果 x == 0,结果设为 32

ret # 返回结果

或者,在稍旧的处理器上,可能使用 `BSR` (Bit Scan Reverse) 指令,它找到最高设置位的位置,需要额外计算才能得到前导零数量:assembly

count_leading_zeros:

testl %edi, %edi

je .L_zero_case

bsrl %edi, %eax # BSR 找到最高位索引

xorl $31, %eax # (31 - index) 得到前导零数量

ret

.L_zero_case:

movl $32, %eax

ret

```

这个汇编代码直接对应于 .final RTL dump 中看到的指令模式。

  • F. 连接各阶段

    这个例子清晰地展示了 __builtin_clz 的生命周期:

    1. 在 GIMPLE 中,它是一个明确的内建函数调用。
    2. 在 GIMPLE 到 RTL 转换时 (.expand),它被识别并映射到一个表示"计数前导零"的内部 RTL 构造。
    3. 在 RTL 优化后 (.final),这个 RTL 构造仍然存在,但操作数已被分配到物理寄存器。
    4. 在最终的汇编生成阶段,基于 .md 文件中的模式匹配,这个 RTL 构造被成功匹配到目标架构的 lzcntlbsrl 指令,并生成了相应的汇编代码。
  • G. 深层含义与影响

    这个具体的演练过程印证了前面章节的理论描述,展示了通过转储文件进行追踪的可行性。它使得抽象的编译阶段变得具体可见:我们可以亲眼看到一个内建函数调用如何被逐步转换、优化,并最终映射到高效的硬件指令。这种追踪能力对于理解编译器行为、调试性能问题以及验证优化效果至关重要,它将理论知识与实际的编译器输出联系起来。

12. 结论

  • A. 生命周期总结

    GCC 内建函数在 C 程序编译过程中经历了一个复杂的生命周期。它们在源代码中被调用,然后在编译阶段被 GCC 识别。初始转换发生在从 C 代码到 GIMPLE IR 的过程中,此时内建调用被表示出来。在 GIMPLE (Tree-SSA) 层面,它们与各种优化遍交互,可能被常量折叠、简化或内联。随后,GIMPLE 被降低到更接近机器的 RTL IR。在这个转换点,内建函数可能被展开为 RTL 指令序列、映射到预定义的 RTL 命名模式,或者在无法优化或需要运行时支持时生成对 libgcc 或标准库的调用。RTL 层面会进行进一步的、更细粒度的优化,如指令合并、调度和寄存器分配。最后,通过查询目标机器描述文件 (.md),RTL 指令模式被匹配并翻译成目标架构的特定汇编指令序列。

  • B. 关键要点

    本次分析的核心要点包括:

    1. 语义驱动优化: 内建函数的核心价值在于向编译器提供精确的语义信息,从而驱动更深层次的优化,这是处理不透明库函数调用时无法实现的。
    2. 上下文依赖处理: GCC 对内建函数的处理是动态和上下文相关的,取决于优化级别、函数参数(如是否为常量)以及目标平台的特性。编译器在编译时权衡利弊,决定是内联展开、使用硬件指令还是回退到库调用。
    3. 硬件映射: 许多内建函数旨在直接利用高效的硬件指令(如 popcnt, lzcnt, SIMD 指令),但这种映射依赖于目标架构的支持以及正确的编译选项(如 -march)。
    4. libgcc 后备: libgcc 运行时库为内建函数提供了重要的后备机制,处理硬件不支持的操作或编译器决定不内联的情况。
    5. IR 的作用: GIMPLE 和 RTL 作为中间表示,在内建函数的转换和优化过程中扮演了关键角色,提供了不同抽象层次的表示以支持各种优化算法。
    6. .md 文件的重要性: 机器描述文件是连接 RTL 和最终汇编代码的纽带,其质量直接影响内建函数(及所有代码)能否被高效地映射到目标硬件。
  • C. 最终思考

    GCC 内建函数的处理机制展现了现代优化编译器设计的精妙之处。它体现了在多语言、多目标环境下,通过精心设计的内建函数接口、多阶段中间表示、复杂的优化遍以及目标特定的机器描述,编译器能够将高级语言的抽象请求与底层硬件的高性能潜力有效地结合起来。理解这一过程不仅对于追求极致性能的 C 程序员至关重要,也为编译器研究人员和开发者提供了宝贵的视角,揭示了在抽象、优化与目标适应性之间取得平衡的复杂艺术。掌握 GCC 内建函数的生命周期,是深入理解编译技术和进行高性能计算的关键一步。

相关推荐
RaLi和夕4 小时前
单片机学习笔记9.数码管
汇编·笔记·单片机·嵌入式硬件·学习
手打猪大屁4 天前
ARM裸机开发——I.MX6U_汇编LED灯驱动
汇编·arm开发
zhmc5 天前
Keil A51汇编伪指令
汇编
攻城狮7号6 天前
【第48节】探究汇编使用特性:从基础到混合编程
汇编·c++·windows
打工人你好11 天前
Visual Studio Code 在.S汇编文件中添加调试断点及功能简介
汇编·ide·vscode
红白小蛋糕12 天前
《操作系统真象还原》第八章(1)——内存管理系统
汇编·笔记·ubuntu
tjsoft13 天前
asm汇编源代码之按键处理相关函数
汇编
tjsoft14 天前
asm汇编源代码之-汉字点阵字库显示程序源代码下载
汇编
AntHub15 天前
汇编获取二进制
汇编