汇编与静态链接

描述操作数编码、节放置、符号绑定和执行入口点。

Wave Foundation

组件和对象

Whale汇编器将AMD64指令转换为机器代码字节和重定位信息。 ELF64 对象包含此信息和部分/符号。汇编由Whale自己的实现执行,不需要外部汇编器。

可重定位对象可能具有最终地址未知的引用。解析这个地址的过程就是一个链接。成功的汇编不应推断所有外部符号已被解析或可执行文件已被创建。

组装函数

将以下代码保存为answer.asm。

section .text
global answer

answer:
    mov eax, 42
    ret
whale asm --amd64 answer.asm -o answer.o

.text的内容是b8 2a 00 00 00 c3,mov eax, 42后面是ret。对象 ELF64 公开 answer。它是一个没有进程启动代码的可调用函数,不是可执行文件。本例中的指令可以由当前的汇编器处理。

使用 Rust API 构造对象

这是将相同功能字节写入板条箱object的完整示例。指定输出目标和全局符号。

use object::{ObjectFile, ObjectSymbol, SectionKind, SymbolBinding, SymbolVisibility, Target};

fn main() {
    let mut object = ObjectFile::with_target(Target::X86_64WhaleLinux.object_target());
    let text = object.add_section(".text", SectionKind::Text, 16);
    object.sections[text].data = vec![0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3];
    object.symbols.push(ObjectSymbol {
        name: "answer".into(),
        section_index: Some(text),
        value: 0,
        size: 6,
        binding: SymbolBinding::Global,
        visibility: SymbolVisibility::Default,
    });
    let elf = object.write().unwrap();
    assert_eq!(&elf[..7], b"\x7fELF\x02\x01\x01");
    assert_eq!(u16::from_le_bytes([elf[18], elf[19]]), 62);
    std::fs::write("answer.o", elf).unwrap();
}

Assertion 检查 ELF 类、字节顺序、machine 标识符。 value: 0 是offset 内的.text,size: 6 是符号的字节大小。无效的节引用或范围是序列化错误。拒绝其他machine或字节顺序,而不将它们标记为AMD64。

解释两个物体的符号

目前linker crate提供符号解释。以下可执行示例在两个对象上定义本地 helper,然后检查如果相同的名称发布两次是否会发生错误。

use linker::core::symbol_table::{SymbolKey, SymbolTable};
use object::{
    ObjectFile, ObjectFormat, ObjectSymbol, SectionKind, SymbolBinding, SymbolVisibility,
};

fn input(binding: SymbolBinding) -> ObjectFile {
    let mut object = ObjectFile::new(ObjectFormat::ELF64);
    let text = object.add_section(".text", SectionKind::Text, 1);
    object.sections[text].data = vec![0xc3];
    object.symbols.push(ObjectSymbol {
        name: "helper".into(),
        section_index: Some(text),
        value: 0,
        size: 1,
        binding,
        visibility: SymbolVisibility::Default,
    });
    object
}

fn main() {
    let mut symbols = SymbolTable::new();
    symbols
        .resolve(&[input(SymbolBinding::Local), input(SymbolBinding::Local)])
        .unwrap();
    for object_index in 0..2 {
        let key = SymbolKey::Local {
            object_index,
            name: "helper".into(),
        };
        assert_eq!(symbols.symbols[&key].object_index, Some(object_index));
    }
    let error = symbols
        .resolve(&[input(SymbolBinding::Global), input(SymbolBinding::Global)])
        .unwrap_err();
    assert_eq!(error, "Duplicate global symbol: helper");
    println!("{error}");
}

输出:

Duplicate global symbol: helper

这两个本地定义具有不同的object_index的单独键。这两个全局定义是冲突的。这个例子直接解释了你在内存中构造的对象的符号。 .o 不执行读取文件、应用重定位或输出可执行文件。在下面描述的完整定义选择策略中,weak优先级等尚未实施。

文字和内存操作数

文字保留其宽度和标牌,直到检查实际指令的编码范围。超出范围的值应该导致错误,而不是被默默地截断。

当无法从命令中的其他信息确定内存宽度时,需要明确的大小符号。例如,寄存器操作数可以确定宽度,但是如果仅存在存储器和立即值,则这可能是不明确的。汇编器不应随机猜测不明确的宽度。

AMD64中由一个符号组成的内存操作数基本上是RIP-relative。选择显式寻址方法rel/abs。文字中未知的 escape 是一个错误。

剖面和对齐方式

栏目内容 对齐行为
代码 NOP 插入命令
初始化数据 插入0字节
BSS 增加逻辑内存大小而不添加文件payload

文件大小和内存大小之间存在区别。 BSS保留内存,但不要求目标文件中存储相同大小的0字节。自定义部分具有维护绑定和类型信息的属性和符号。

Payload 预留 BSS 未分配

将以下内容另存为buffer.asm。逻辑BSS保留1个TiB,但在汇编时不会赋值或写入1个TiB。

section .bss
global buffer
buffer:
    resb 1099511627776
buffer_end:

section .text
    ret
whale asm --amd64 buffer.asm -o buffer.o

buffer段内的值为0,buffer_end的值为1099511627776。.bss标头是具有该大小的SHT_NOBITS,并且没有文件payload。之后,当您返回.bss时,继续使用逻辑offset。带零的数据指令也会增加逻辑大小。非零初始值、BSS内应用的重定位以及BSS内的命令将被拒绝。

同样的区别也可用于对象和链接器API。

use linker::core::layout::Layout;
use object::{ObjectFile, ObjectFormat, SectionKind};

fn main() {
    let mut object = ObjectFile::new(ObjectFormat::ELF64);
    let text = object.add_section(".text", SectionKind::Text, 16);
    object.sections[text].data = vec![0xc3];
    let bss = object.add_section(".bss", SectionKind::Bss, 16);
    object.sections[bss].zero_fill = 1 << 40;
    assert!(object.sections[bss].data.is_empty());

    let elf = object.write_with_limit(4096).unwrap();
    assert!(elf.len() < 4096);
    let layout = Layout::compute(&[object], 0x1000).unwrap();
    assert_eq!(layout.file_size, 1);
    assert_eq!(layout.memory_size, (1 << 40) + 16);
    assert_eq!(layout.sections[bss].memory_address, 0x1010);
    assert_eq!(layout.sections[bss].file_size, 0);
}
section  memory address  file bytes       memory bytes
.text    0x1000          1                1
.bss     0x1010          0                1099511627776

Section::zero_fill 是未存储在文件中的额外 BSS 字节数。测试的内存大小为data.len() + zero_fill。现有的零填充 BSS data 也被接受,但可以使用空 data 向量来避免其分配。除 BSS 之外的部分必须为 zero_fill == 0。物理0存储空间并不意味着IR的初始化状态。

Layout::compute返回Result,记录了输入对象/节的对应关系、对齐方式、文件offset、内存地址以及所有节的大小。检查地址和对齐算法,保留输入顺序,并将对象对齐 0 视为不受约束(对齐 1)。 BSS 不移动文件光标。这是 payload 部署,具有可执行标头,load segment 具有访问权限,应用重定位是一项单独的任务。

ELF writer 拒绝 overflow 以及减小字段宽度时的截断。不支持扩展节号,并且包含创建的表和重排节的表头总数必须小于0xff00。序列化输出的默认限制是 256 MiB。您可以使用ObjectFile::write_with_limit或write_elf_with_limit指定包括padding·表的字节限制,并且该限制不包括未存储在文件中的BSS内存大小。在分配最终字节向量之前检查输出大小。

符号识别

函数和变量在IR内使用不同的标识符。外部连接使用前端指定的link_name。 Whale 不会自动重命名冲突的公共符号之一,保留其声明的名称。

IR声明表记录类型化的FunctionId引用和显式函数link_name值。下面的汇编器/对象/链接器 API 仍然是单独的接口:本机 IR 发射尚未将这些身份传递到最终链接。 IR 单独的调用验证并不能建立这种端到端属性。

因此,内部函数和变量都可以具有名称item,但使用相同的外部名称公开两者将是错误的。分离内部命名空间不会自动分离外部命名空间。

对象局部符号的范围是其输入对象。全局符号参与对象之间的解释。已确认的功能/数据冲突属于错误。 NOTYPE 符号保持与不提供更具体类型的输入的兼容性。它没有类型这一事实并不意味着它是函数或数据。

选择定义

定义或参考 结果
Strong 和 strong 重复定义错误
Strong 和 weak Strong 选择定义
Weak 和 weak 选择输入顺序中的第一个定义
请参阅strong未解决 链接错误
查看未解决weak 初始静态配置文件不支持错误

如果有多个weak定义,它们的输入顺序将影响结果。对于确定性链接,您必须一致地使用传递的输入顺序。

静态可执行输出

静态 native 配置文件生成指定入口点的 ELF ET_EXEC。入口点不是从函数名称main推断出来的。它也不会自动插入调用该函数的启动代码。

它不会自动删除部分、合并相同的代码或删除符号。文件放置需要单独计算实际保存的字节和运行时保留的内存。

CLI 的完整静态可执行文件创建路径尚不可用。 whale asm 创建一个可重定位对象,并且 whale object 将原始字节包装到一个对象中。请参阅工具链概述了解可用性,ABI和AMD64 目标了解要求。

可选Wave记录序列化

Linux Whale x86_64 的构建现在可以使用 Wave 实现来固定 ELF64 header·section·symbol·RELA 记录。还提供了默认的 Rust 实现。输出目标选择、对象验证、放置、符号解释和缓冲区分配由Rust处理。选择 Wave 不会添加支持的架构或完整链接器。

安装 Rust、LLVM 21 个开发库、C 链接器和 ar,然后从 Whale 存储库构建选定的路径。

git clone https://github.com/wavefnd/Wave.git /tmp/whale-wave-bootstrap
git -C /tmp/whale-wave-bootstrap checkout --detach 8a465e30aeea4b817d925cdd0e8d08c1bb029c9a
python3 tools/build_wave_elf.py --wave-source /tmp/whale-wave-bootstrap --out-dir /tmp/whale-wave-elf
WHALE_WAVE_ELF_DIR=/tmp/whale-wave-elf cargo build --locked --all-features
WHALE_WAVE_ELF_DIR=/tmp/whale-wave-elf cargo test --locked --workspace --all-features

该脚本检查固定revision,拒绝对tracked的更改,构建Wave编译器,使用LLVM创建Wave对象,并将其与archive捆绑在一起以进行静态链接。 WHALE_WAVE_ELF_DIR 应该有这个archive。明确请求无效的 archive 或不受支持的主机是构建错误。未指定变量的正常构建不需要 --all-features 或 Wave 编译器。链接的 Whale 可执行文件在运行时也不需要 Wave 编译器。尚不支持将 Whale 本身与 bootstrap 交叉编译的路径。

例如,将以下内容保存为 return.asm 并将其汇编到生成的二进制文件中:

section .text
global entry
entry:
    ret
target/debug/whale asm --amd64 return.asm -o return.o
ELF class: ELF64
machine: AMD64 (62)
.text bytes: c3

记录ABI携带类型、u64字段指针、字段数量、输出指针和容量。该缓冲区由调用者Rust拥有。它不会跨边界传递赋值所有权或 Rust enum/String/Vec 表达式。 Wave 例程在写入之前检查计数、容量和字段宽度。成功返回状态 0,无效类型/指针/容量返回 1,字段 overflow 返回 2。指针必须指向活动的、大小正确的缓冲区,并且彼此不重叠。原始的C指针本身无法证明这个条件; wrapper满足。该测试将整个 ELF(包括 BSS 和 signed 重定位 addend)与 Rust 路径进行比较。这是使用 Wave/LLVM bootstrap 的部分 Wave 实现,并且不是完全自托管的。