Небольшая заметка-эксперимент о том, какой минимальный размер исполняемого файла можно получить в Rust под Windows. Уменьшим размер с 145 408 байт до 768 байт !

Создаем папку на диске. В папке через терминал выполняем

$ cargo init

Создадутся необходимые файлы проекта и такой код в main.rs:

fn main() {
    println!("Hello, world!");
}

Делаем cargo build –release и получаем исполняемый файл размером 145 408 байт, что очень много. Такой большой размер из-за того, что в файле отладочная информация + runtime Rust (std библиотека и прочее)

Для начала в файле Cargo.toml пропишем ряд параметров как описано здесь.

[package]
name = "rust_min"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

#добавили параметры профиля
[profile.release]
opt-level = "z"
debug = false
lto = true
codegen-units = 1
panic = "abort"
strip = "debuginfo"
incremental = false
debug-assertions = false
overflow-checks = false
default-features = false

Размер стал 127 488 байт. Не сильно нам помогли параметры. Это все равно очень много для простого вывода строки в консоль. Поэтому уберем std через #![no_std].

#![no_std]
#![no_main]

#[link(name = "ucrtd")]
extern "C" {
    pub fn printf(format: *const i8, ...) -> i32;
}

#[no_mangle]
pub extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
    let hello = "Hello, minimal Rust!\n\0";
    unsafe {
        printf(hello.as_ptr() as *const _);
    }
    0
}

#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

Т.к. стандартной библиотеки нет, то будем использовать С функцию printf. Для этого линкуем ucrtd.lib и прописываем определение импортируемой функции. Библиотеки Windows описаны здесь. Я выбрал ucrtd – динамически линкуемую C STL библиотеку (почему называется STL – написано по той же ссылке). Почему ucrtd, а не ucrt объясню позже.

Далее нам нужно создать в папке проекта директорию .cargo и в ней файл config.toml следующего содержания:

[target.'cfg(target_env="msvc")']
rustflags = [
    "-C", "link-args=/entry:main",
]

Здесь мы указываем линковщику из msvc какая функция является входной точкой нашего приложения. Собираем исполняемый файл и получаем размер 2 560 байт. Уже неплохо, но нужно стремится хотя бы к 1 Кб, поэтому исследуем структуру файла через дизассемблер, что бы понять на что уходит место.

Первое что бросается в глаза, это то, что в файле очень много места просто пустого

Попробуем уменьшить размер выравнивания секций через флаг ALIGN.

[target.'cfg(target_env="msvc")']
rustflags = [
    "-C", "link-args=/entry:main /ALIGN:8",
]

И вот тут оказалось, что библиотека ucrtd позволяет поставить значение 8, а обычная ucrt минимум 16. Собираем приложение и получаем размер 1064 байт. Отличный результат!, но можно еще уменьшить.

Смотрим опять дизассемблером. Первые 568 байт заняты PE заголовком. Как его уменьшить через флаги компилятор/линковщика я не нашел.

Далее видим блок IMAGE_DEBUG_TYPE_POGO

Хотя мы и отключили отладочную информацию, все равно в файле есть какие то отладочные структуры. В процессе поиска информации по этому блоку (а информации можно сказать нет никакой) выяснилось, что есть недокументированный флаг линковщика msvc /EMITPOGOPHASEINFO. Пропишем его:

[target.'cfg(target_env="msvc")']
rustflags = [
    "-C", "link-args=/entry:main /ALIGN:8 /EMITPOGOPHASEINFO",
]

Собираем исполняемый файл и получаем размер 768 байт, из которых 568 байт это заголовок PE. Часть места так же занимает таблица импорта из библиотек и сами секции PE файла, но как их убрать через флаги линковщика я не нашел.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *