Ассемблирование и статическая компоновка

Описывает кодировку операндов, размещение разделов, привязку символов и точки входа выполнения.

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 — это .text внутри offset, а size: 6 — размер символа в байтах. Недопустимые ссылки на разделы или диапазоны являются ошибками сериализации. Отклоняет другие machine или порядки байтов, не отмечая их как AMD64.

Интерпретировать символы двух объектов

В настоящее время ящик linker обеспечивает интерпретацию символов. В следующем примере исполняемого файла определяется локальный 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. Вы можете указать ограничение в байтах, включая таблицу padding· с помощью ObjectFile::write_with_limit или write_elf_with_limit, и ограничение не включает размер памяти 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, создает объект Wave с LLVM и связывает его с archive для статического связывания. WHALE_WAVE_ELF_DIR должен иметь это archive. Явный запрос недопустимого archive или неподдерживаемого хоста является ошибкой сборки. Обычные сборки без указанных переменных не требуют компиляторов --all-features и Wave. Компилятор Wave также не требуется во время выполнения связанного исполняемого файла Whale. Путь для кросс-компиляции 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, которая использует Wave/LLVM bootstrap и не является полностью автономной.