GNU 汇编器

编译流程与ELF文件

GCC编译流程

先看一段最简单的 C 代码:

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

int data = 10;

int main(void) {
    printf("%d\n", data);
    return 0;
}

GCC 编译 4 个阶段,每一步都生成一个新文件:

阶段 命令 作用 输出文件
预处理 gcc -E test.c -o test.i 处理头文件、宏定义、条件编译 预处理后的 .i 文件
编译 gcc -S test.i -o test.s 词法 / 语法分析,翻译成汇编指令 汇编 .s 文件
汇编 as test.s -o test.o 把汇编指令转成机器码 可重定位目标文件 .o
链接 ld -o test test.o -lc 合并 .o + 库文件,解决地址引用 可执行二进制文件

ELF文件

不管是 .o 目标文件,还是最终的 test 可执行文件,它们都遵循 ELF(Executable and Linkable Format)标准格式,Linux 系统里的可执行文件几乎都是这个格式。它的结构可以分成三大块:

① ELF 文件头(ELF header):整个文件的"身份证"

包含:文件类型(32/64 位)、目标架构(比如 AArch64)、程序入口地址、段头 / 程序头的位置和数量等基础信息。

bash 复制代码
rlk@rlk:test$ readelf -h elf_read
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14744 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

② 程序头表(Program header table):给操作系统用的"加载说明书"

它告诉内核:把文件里的哪些部分加载到内存的什么位置、需要什么权限(读 / 写 / 执行)。只有可执行文件才有完整的程序头,目标文件一般没有。

③ 段头表(Section header table):给链接器 / 调试器用的"文件目录"

它记录了文件里所有"段(section)"的信息,比如段的名字、在文件中的偏移、大小、读写权限等。

bash 复制代码
rlk@rlk:test$ readelf -S elf_read
  [16] .text             PROGBITS         0000000000001060  00001060
       0000000000000195  0000000000000000  AX       0     0     16
  [18] .rodata           PROGBITS         0000000000002000  00002000
       0000000000000008  0000000000000000   A       0     0     4
  [25] .data             PROGBITS         0000000000004000  00003000
       0000000000000014  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000004014  00003014
       0000000000000004  0000000000000000  WA       0     0     1

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

ELF里常见的段

你可以把这些段理解成文件里的"不同分区",每个分区存不同类型的数据:

段名 全称 作用 权限
.text 代码段 存放你写的代码编译后的机器指令,比如 main 函数、printf 函数的指令 只读 + 可执行
.rodata 只读数据段 存放程序里的常量,比如字符串 "hello world"、宏定义的常量 只读
.data 数据段 存放已初始化的全局变量 / 静态变量,比如你代码里的 int data = 10; 可读 + 可写
.bss 未初始化数据段 存放未初始化的全局变量 / 静态变量,运行时才会分配内存并清零 可读 + 可写
.symtab 符号表 存放函数、变量的符号信息,链接器用它解决外部引用,调试器也靠它定位符号 -
.rel.text/.rel.data 重定位段 存放链接时需要修改的地址信息,比如外部函数调用的地址占位符 -
.debug 调试段 存放调试信息,比如变量名、行号,gdb 调试时会用到 -

目标文件和可执行文件的区别

① .o 目标文件

  • 多个 .o 文件是相互独立的,每个文件里的函数 / 变量地址都是 0(还没确定最终地址)
  • 没有程序头表,段的起始地址都是 0
  • 有重定位段,记录了所有需要链接时修改的地址

② test 可执行文件

  • 链接器把多个 .o 文件合并,给每个段分配了最终的虚拟地址,比如 .text 段的地址是 0x4002E0
  • 生成了完整的程序头表,告诉操作系统怎么加载这个程序到内存
  • 解决了所有外部符号引用,重定位段的任务已经完成,部分可执行文件会去掉重定位信息

一个简单的汇编程序

下面通过一个完整的 ARM64 汇编程序,把前面讲的 ELF 段、符号表、编译链接流程串起来。这段代码定义了两个全局变量,把它们相加后用 printf 打印出来。

asm 复制代码
.section .data
.align 3
my_data1:        .quad 100
my_data2:        .word 50
print_data:      .string "data: %d\n"

.align 3
.section .text
.global main
main:
        stp x29, x30, [sp, #-16]!
        ldr x5, =my_data1
        ldr x2, [x5]
        ldr x6, =my_data2
        ldr x3, [x6]
        add x1, x2, x3
        ldr x0, =print_data
        bl printf
        ldp x29, x30, [sp], #16
        ret

代码解析

① 数据段定义(第 2-13 行)

asm 复制代码
.section .data
.align 3
my_data1: .quad 100   // 64位变量,值为100,地址8字节对齐
my_data2: .word 50     // 32位变量,值为50
print_data: .string "data: %d\n"  // 格式化字符串

这部分定义了程序运行时需要的数据,全部会被存放在 .data 段中。

② 代码段主体(第 14-33 行)

asm 复制代码
.section .text
.global main
main:
    stp x29, x30, [sp, -16]!  // 保存栈帧和返回地址(进入函数前的标准操作)
    // 加载 my_data1 的值到 x2
    ldr x5, =my_data1         // 把 my_data1 的地址加载到 x5
    ldr x2, [x5]              // 从 x5 地址读取 64 位数据到 x2 → x2 = 100
    // 加载 my_data2 的值到 x3
    ldr x6, =my_data2         // 把 my_data2 的地址加载到 x6
    ldr x3, [x6]              // 从 x6 地址读取 32 位数据到 x3 → x3 = 50
    add x1, x2, x3            // x1 = x2 + x3 = 100 + 50 = 150(printf的第二个参数)
    ldr x0, =print_data       // 把格式化字符串地址加载到 x0(printf的第一个参数)
    bl printf                 // 调用C库的printf函数,打印 "data: 150"
    ldp x29, x30, [sp], 16    // 恢复栈帧和返回地址
    ret                       // 函数返回

这部分代码的核心逻辑非常清晰:保存函数调用上下文(栈帧、返回地址);读取两个全局变量的值并相加;调用 printf 打印结果;恢复上下文并返回。

符号表解读

使用 readelf -s test 命令用来查看 ELF 文件的符号表,里面记录了所有符号的地址、类型、绑定属性等信息。

① 关键符号解读

符号名 地址 类型 作用
my_data1 0x411008 LOCAL 数据段中定义的 64 位变量,值为 100
my_data2 0x411010 LOCAL 数据段中定义的 32 位变量,值为 50
print_data 0x411018 LOCAL 只读数据段中的格式化字符串
main 0x4002B0 GLOBAL 程序入口函数,链接器通过这个符号找到入口
_edata / _end 0x411020 GLOBAL 链接器自动生成的符号,标记数据段 / 程序的结束地址

② 为什么这些符号的地址不一样?

main 在 .text 段,地址从 0x400000 开始;my_data1/my_data2 在 .data 段,地址从 0x411000 开始。这是因为链接器把不同段分配到了不同的虚拟地址区域,代码段和数据段分开存储,保护程序安全。

汇编语法

注释

GNU 汇编支持两种注释方式:

① 单行注释

  • //:和 C 语言一样,注释从 // 到行尾
  • #:也可以注释整行或行尾,是汇编里更常见的写法

② 多行注释

  • /* ... */:和 C 语言一样,中间内容跨越多行也会被当成注释

符号(Symbol)

符号是汇编里的核心概念,相当于给地址起的名字,链接器、调试器都靠它工作。

① 符号的构成规则

  • 只能由以下字符组成:大小写字母(a-z, A-Z)、数字(0-9)、下划线 _、点号 .、美元符号 $

② 符号的作用

  • 标记代码 / 数据的地址,比如 main:、my_data:
  • 链接器靠它解决跨文件引用(比如 C 调用汇编函数)
  • 调试器靠它定位代码位置

③ 符号的分类

类型 声明方式 作用 特点
全局符号 .global 符号名 可以被其他文件引用,比如 main 函数 会出现在 ELF 符号表中
本地符号 通常用 .L 前缀,比如 .Lloop 只在当前文件内部使用 不会出现在符号表,调试时不可见

本地标签(Local Label)

① 什么是本地标签?

  • 用 0~99 的数字定义,比如 1:、2:
  • 只在当前汇编文件内有效,可以重复定义
  • 配合 f(forward,向前)和 b(backward,向后)使用

② f 和 b 怎么用?

  • Nf:向前搜索,找后面最近的 N: 标签(向前就是向下)
  • Nb:向后搜索,找前面最近的 N: 标签(向后就是向上)

常用的伪指令

伪指令不是真正的机器指令,而是给汇编器看的预处理命令,只在汇编阶段起作用,不会生成可执行代码。它的作用包括:

  • 定义符号、数据、段
  • 控制汇编流程(条件编译、循环)
  • 定义宏(重复代码模板)

.align 对齐伪指令

作用:让代码 / 数据按指定字节数对齐,提升 CPU 访问效率,避免未对齐异常。

语法:.align n , fill \[, maxskip]

  • n:对齐参数,在 ARM64 中表示按 2^n 字节对齐。比如 .align 2 → 按 2^2=4 字节对齐;.align 5 → 按 32 字节对齐。
  • fill:填充字节,省略时默认用 0,代码段默认用 nop。
  • maxskip:对齐时最多跳过的字节数,超过则不执行对齐。

例子解析

asm 复制代码
.align 2        // 按4字节对齐,默认填充0/nop
.align 5,0,100  // 按32字节对齐,填充0,最多跳过100字节
.align 5,0,8    // 按32字节对齐,但最多跳过8字节,不足则不执行

数据定义伪指令

用来在 .data/.rodata 段定义数据,直接把值编译进目标文件。

伪指令 作用 大小
.byte 定义 8 位数据 1 字节
.hword/.short 定义 16 位数据 2 字节
.word/.long/.int 定义 32 位数据 4 字节
.quad 定义 64 位数据 8 字节
.float/.double 定义浮点数 4/8 字节
.ascii 定义字符串,不自动加 \0 按字符数
.asciz/.string 定义字符串,自动加 \0 字符数 + 1

① .rept/.endr:重复执行

用来重复一段代码 / 数据定义,减少重复输入。

asm 复制代码
.rept 3        // 重复3次
.long 0        // 定义3个0
.endr

等价于:

asm 复制代码
.long 0
.long 0
.long 0

② .equ:给符号赋值

相当于 C 语言的 #define,给符号定义一个常量值。

asm 复制代码
.equ my_data1, 100  // my_data1 = 100
.equ my_data2, 50   // my_data2 = 50

main:
    ldr x2, =my_data1  // 加载100
    ldr x3, =my_data2  // 加载50
    add x1, x2, x3     // x1 = 150

与段相关的伪指令

用来组织代码和数据到不同的 ELF 段中,链接器会根据段属性分配内存。

① .section:声明段

格式:.section name, "flags"

  • name:段名,比如 .text(代码段)、.data(数据段)
  • flags :段属性(可组合)
    • a:可分配
    • w:可写
    • x:可执行
    • M:可合并
    • S:包含零终止字符串

例子:

asm 复制代码
.section .idmap.text, "awx"  // 声明一个可分配、可写、可执行的段

② .pushsection/.popsection:临时切换段

可以临时切换到另一个段,执行完后再切回原来的段,适合在函数中间插入不同段的代码。

asm 复制代码
.section .text
.global atomic_add
atomic_add:
    ......
    ret  // 属于.text段

.pushsection ".idmap.text", "awx"  // 临时切换到新段
.global atomic_set
atomic_set:
1:  ldxr x2, [x1]
    orr x2, x2, x0
    strx w3, x2, [x1]
    cbnz w3, 1b
    ret  // 属于.idmap.text段
.popsection  // 切回原来的.text段

宏定义伪指令 .macro/.endm

用来定义汇编宏,相当于"代码模板",调用时会展开成具体指令,适合减少重复代码。

① 基本宏定义

asm 复制代码
.macro add_data p1 p2  // 宏名 + 参数
    mov x5, \p1        // 参数前加反斜杠\
    mov x6, \p2
    add x1, x5, x6
.endm

// 调用宏
add_data x2, x3  /*展开为:mov x5, x2; mov x6, x3; add x1, x5, x6*/

② 宏参数默认值

可以给参数设置默认值,调用时省略则使用默认值。

asm 复制代码
.macro add_data p1=0 p2
    mov x5, \p1
    mov x6, \p2
    add x1, x5, x6
.endm

add_data , x3    // p1用默认值0,x1=0+x3
add_data x2, x3  // p1=x2,x1=x2+x3

③ 必传参数 .req

在参数后加 .req,表示调用时必须传值,否则编译报错。

asm 复制代码
.macro add_data_1 p1:req p2
    mov x5, \p1
    mov x6, \p2
    add x1, x5, x6
.endm

add_data_1 , x3  // 报错:缺少必传参数p1

④ 宏参数拼接技巧

用 \() 告诉汇编器宏参数的结束位置,实现字符串拼接。

asm 复制代码
.macro kernel_ventry, el, label
    b el\()\el\()_\label  // 拼接成 el1_irq 这样的标签
.endm

// 调用
kernel_ventry 1, irq  // 展开为:b el1_irq

基础伪指令:.global 和 .include

① .global(也写作 .globl)

作用:定义一个全局符号,可以是函数,也可以是全局变量。核心用途:让其他文件(比如 C 代码)可以调用或引用这个符号,链接器能找到它。

asm 复制代码
.global main   // 声明main为全局符号,链接器会把它作为程序入口
.global my_var // 声明my_var为全局变量,C代码可以用extern int my_var引用

② .include

作用:在汇编文件中引入头文件,和 C 语言的 #include 用法一样。

asm 复制代码
#include "asm/uart.h"  // 引入uart寄存器定义的头文件
#include "base.h"      // 引入基础宏定义

条件编译伪指令

这些指令是汇编器在预处理阶段处理的,用来实现条件编译,根据条件决定哪些代码被编译,哪些被忽略。

① 基础条件结构:.if / .else / .endif

语法:和 C 语言的 #if/#else/#endif 几乎一样。

asm 复制代码
.if CONFIG_BOARD_PI3B    // 只有定义了CONFIG_BOARD_PI3B时,这段代码才会被编译
    .word 0x1234
.else    // 否则编译这段
    .word 0x5678
.endif

② 符号存在判断:.ifdef / .ifndef

指令 作用
.ifdef symbol 如果 symbol 被定义了,就编译后面的代码
.ifndef symbol 如果 symbol 没有被定义,就编译后面的代码
asm 复制代码
// 如果定义了DEBUG,就开启调试代码
.ifdef DEBUG
    .global debug_print
debug_print:
    ret
.endif

③ 字符串比较:.ifc / .ifeqs

作用:判断两个字符串是否相等。

  • .ifc string1, string2:如果两个字符串相等,编译后面的代码
  • .ifeqs string1, string2:和 .ifc 完全等价,只是别名
asm 复制代码
.ifc "arm64", "arm64"    // 字符串相等,编译这段
    .arch armv8-a
.endif

④ 数值表达式判断:.ifeq / .ifge / .ifle / .ifnc

这些指令用来判断数值表达式的值,汇编器会先计算表达式的值,再判断条件。

指令 判断条件 说明
.ifeq expression 表达式的值为 0 等于 0 时编译
.ifge expression 表达式的值 ≥ 0 大于等于 0 时编译
.ifle expression 表达式的值 ≤ 0 小于等于 0 时编译
.ifnc expression 表达式的值 ≠ 0 不等于 0 时编译(和 .ifeq 相反)
asm 复制代码
.equ MY_VAL, 10

.ifeq MY_VAL - 10    // 10-10=0,条件成立,编译这段
    .word 0
.endif

.ifge MY_VAL - 5    // 10-5=5≥0,条件成立,编译这段
    .word 1
.endif

AArch64 特性

AArch64 汇编器的特有命令选项

这些是 as 汇编器的命令行参数,用来控制目标架构、端序、ABI 等编译行为:

选项 说明
-EB 大端(big-endian)模式编码
-EL 小端(little-endian)模式编码(ARM 默认)
-mabi=abi 指定 ABI,AArch64 常用 lp64(64 位指针),决定生成 ELF64 格式文件
-mcpu=processor 指定目标处理器,如 cortex-a72,汇编器会检查指令是否兼容
-march=architecture 指定目标架构,如 armv8.1-a,也可搭配扩展(如 +simd)
-mverbose-error 显示详细错误信息
-mno-verbose-error 关闭详细错误信息

AArch64 重定位与 ADRP 指令

这是 AArch64 中最核心的地址加载方式,专门解决 64 位地址的重定位问题。

① 什么是 ADRP?

ADRP(Address of Page)是 PC 相对寻址指令,它的特点:

  • 以当前 PC 为基准,按 4KB 页对齐加载地址
  • 寻址范围:±4GB(足够覆盖绝大多数场景)
  • 加载的是标签所在页的基地址,而不是标签本身的地址

② 如何加载完整地址?

因为 ADRP 只加载页基地址,需要用 #:lo12: 来取页内偏移,组合出完整地址:

asm 复制代码
adrp x0, foo        // 加载foo所在页的基地址(按4KB对齐)
add x0, x0, #:lo12:foo  // 加上foo在页内的偏移,得到foo的完整地址

假设 foo 的地址是 0x8000100,4KB 页基地址是 0x8000000:

  • ADRP 加载 0x8000000
  • #:lo12:foo 提供偏移 0x100
  • 最终 x0 = 0x8000100

AArch64 特有的伪指令

这些伪指令专门为 ARM64 架构设计,用来简化汇编代码:

伪指令 作用
.arch name 设置目标体系结构,如 .arch armv8-a
.arch_extension name 添加 / 移除架构扩展,如 .arch_extension simd
.bss 切换到 .bss 段(未初始化数据段)
.cpu name 设置目标处理器,如 .cpu cortex-a72
.dword/.xword 插入 64 位数据(和 .quad 等价)
.even 输出对齐到下一个偶数字节边界
.inst expressions 将表达式作为指令插入,常用于手动定义机器码
.req name, register 给寄存器定义别名,如 foo .req w0,之后可以用 foo 代替 w0

LDR 伪指令

GNU 汇编器为 AArch64 提供了 ldr reg, =expr 伪指令,用来加载任意地址或常量。

① 伪指令 vs 真实指令

伪指令:ldr x1, =my_label

汇编器会把它翻译成:

  • 把 my_label 的值放到文字池(literal pool)
  • 用一条 PC 相对的 ldr 指令从文字池读取值到寄存器

② 两种用法的区别

asm 复制代码
// 用法1:加载常量(伪指令)
#define MY_CONST 0x30
ldr x1, =MY_CONST  // 汇编器把0x30放进文字池,再用ldr加载

// 用法2:加载数据地址 + 读取数据
my_data: .quad 0x100
ldr x1, =my_data  // 伪指令:把my_data的地址加载到x1
ldr x2, [x1]      // 真实指令:从x1地址读取数据,x2=0x100

第一条 ldr x1, =my_data 是伪指令,加载的是 my_data 的链接地址;第二条 ldr x2, x1 是真实的内存加载指令,读取 my_data 存储的值。

相关推荐
北京阿尔泰科技厂家1 小时前
阿尔泰SC5061八串口联网服务器技术解析与工程应用指南
服务器·物联网·串口服务器·串口通讯·远程监测·自动化控制·工业测试测量
tianyuanwo1 小时前
RPM Spec中Name: %{?scl_prefix}xxx中增加宏定义的设计原理
linux·服务器·rpm·scl
cc_瀚海知行2 小时前
工具调用 6|助手什么时候该停下来?
java·服务器·前端·人工智能·python·ai
红姐跨境书3 小时前
Facebook账号频繁被要求验证怎么办?从登录异常到浏览器环境排查
运维·服务器
月落汀兰3 小时前
服务器出问题没人告警?Zabbix6.0 LNMP 部署,MySQL 监控 + 邮件告警保姆级实战
运维·服务器·zabbix
骇客野人3 小时前
CI/CD A 服务器(构建机)免密登录 B 服务器(目标部署机)完整操作
服务器·servlet·ci/cd
虚无的纽扣3 小时前
【Linux】线程(下):怎么创建、终止与分离一个线程
linux·运维·服务器
FITA阿泽要努力3 小时前
/goal 命令:给 Agent 设定目标
服务器·人工智能·python·agent
晴天的雨.9923 小时前
【Linux】编译器gcc/g++
linux·运维·服务器