Rust должен умереть, МГУ сделал замеры

В предыдущих сериях:

Медленно, но верно Раст проникает не только в умы сотрудников больших корпораций, но и в умы школьников и студентов. В этот раз мы поговорим о статье от студента МГУ: https://rustmustdie.com/.

Её репостнул Андрей Викторович Столяров, доцент кафедры алгоритмических языков факультета ВМК МГУ им. М.В. Ломоносова и по совместительству научрук студента-автора статьи.


Я бы сказал, что тут дело даже не в том, что он «неинтуитивный». Дело скорее в том, что компилятор раста сам решает, когда владение «должно» (с его, компилятора, точки зрения) перейти от одного игрока к другому. А решать это вообще-то должен программист, а не компилятор. Ну и начинается пляска вида «как заставить тупой компайлер сделать то, чего я хочу».
Бред это всё.

— А.В. Столяров

Сама статья короткая, но постулирует довольно большой список спорных утверждений, а именно:


  • Стандартная библиотека неотделима от языка
  • У него отсутствует нулевой рантайм
  • В Rust встроен сборщик мусора
  • Компилятор генерирует медленный машинный код

На самом деле набросов еще больше, но достаточно и этого списка.

К сожалению, для опровержения этих пунктов мне придется писать максимально уродские хэлло ворлды, которые только можно представить.


Нулевой рантайм в Си

Честно говоря, до прочтения статьи я ни разу не встречал такого определения как zero runtime. Немного погуглив, я наткнулся на книгу А.В. Столярова ISBN 978–5–317–06575–7 Программирование: введение в профессию. II: Системы и сети, изданной в 2021 году. В главе »§4.12: (*) Программа на Си без стандартной библиотеки» приводится определение нулевого рантайма и пример программы.

Реализация подпрограммы _start (под Linux i386):


start.asm
global _start       ; no_libc/start.asm
extern main
section     .text
_start:
    mov ecx, [esp]  ; argc in ecx
    mov eax, esp
    add eax, 4      ; argv in eax
    push eax
    push ecx
    call main
    add esp, 8      ; clean the stack
    mov ebx, eax    ; now call _exit
    mov eax, 1
    int 80h

Модуль с «обертками» для системных вызовов:


calls.asm
global sys_read     ; no_libc/calls.asm
global sys_write
global sys_errno

section .text

generic_syscall_3:
    push ebp
    mov ebp, esp
    push ebx
    mov ebx, [ebp+8]
    mov ecx, [ebp+12]
    mov edx, [ebp+16]
    int 80h
    mov edx, eax
    and edx, 0fffff000h
    cmp edx, 0fffff000h
    jnz .okay
    mov [sys_errno], eax
    mov eax, -1
.okay:
    pop ebx
    mov esp, ebp
    pop ebp
    ret

sys_read:
    mov eax, 3
    jmp generic_syscall_3

sys_write:
    mov eax, 4
    jmp generic_syscall_3

section .bss
sys_errno resd 1

Простенькая программа, которая принимает ровно один параметр командной строки, рассматривает его как имя и здоровается с человеком, чьё имя указано, фразой Hello, dear NNN (имя подставляется вместо NNN):


greet3.c
/* no_libc/greet3.c */
int sys_write(int fd, const void *buf, int size);

static const char dunno[] = "I don't know how to greet you\n";
static const char hello[] = "Hello, dear ";

static int string_length(const char *s)
{
  int i = 0;
  while(s[i])
    i++;
  return i;
}

int main(int argc, char **argv)
{
  if(argc < 2) {
    sys_write(1, dunno, sizeof(dunno)-1);
    return 1;
  }
  sys_write(1, hello, sizeof(hello)-1);
  sys_write(1, argv[1], string_length(argv[1]));
  sys_write(1, "\n", 1);
  return 0;
}

И сама сборка:

nasm -f elf start.asm
nasm -f elf calls.asm
gcc -m32 -Wall -c greet3.c
ld -melf_i386 start.o calls.o greet3.o -o greet3

На машине автора этих строк (Столярова) размер файла составил 816 байт. На моей машине 13472 байта.

Что ж, применим clang-14, ld.lld-14, -Os и strip; и на моей машине получилось 1132 байта:

nasm -f elf start.asm
nasm -f elf calls.asm
clang-14 -m32 -Os -Wall -c greet3.c
ld.lld-14 -melf_i386 start.o calls.o greet3.o -o greet3
strip ./greet3

В своей книге Столяров делает очень сильное утверждение, а именно:


Но дело даже не в этой экономии (размера исполняемого файла — Прим. авт.)…
Намного важнее сам принцип: язык Си позволяет полностью отказаться от возможностей стандартной библиотеки. Кроме Си, таким свойством — абсолютной независимостью от библиотечного кода, также иногда называемым zero runtime — обладают на сегодняшний день только языки ассемблеров; ни один язык высокого уровня не предоставляет такой возможности.

Что ж, давайте разберемся, обладает ли Раст таким свойством.


Из чего состоит хэлло ворлд

Рассмотрим базовый пример, приведённый на официальном сайте языка Раст:

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

Так как println! — это макрос, а не функция, у нас есть возможность посмотреть на код после раскрытия макроса. Для этого воспользуемся утилитой cargo-expand:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
  {
    ::std::io::_print(::core::fmt::Arguments::new_v1(
      &["Hello, world!\n"],
      &match () {
        _args => [],
      },
    ));
  };
}

Компилятор вставил импорт стандартной библиотеки extern crate std; и прелюдию use std::prelude::rust_2021::*;. Именно эти неявные вставки я и хотел показать.

Стандартная библиотека — это удобный набор функций, коллекций, структур и типажей в окружении, когда у тебя есть ос, фс, куча, сокеты и прочая хипстота. Считается, что 93.9% программистам именно такое поведение (автоматическое включение std и прелюдии) и требуется.

Весь API стандартной библиотеки подробно описан в официальной документации. Есть удобный быстропоиск: https://std.rs/QUERY, где QUERY — ваш запрос, например https://std.rs/mutex.


Отключаем std

Тем не менее, для остальных 19% программистов предусмотрен режим отключения стандартной библиотеки с помощью атрибута #![no_std].

#![no_std]
#![feature(start, lang_items)]

// Говорим компилятору влинковать libc
#[cfg(target_os = "linux")]
#[link(name = "c")]

extern "C" {
  // Объявляем внешнюю функцию из libc
  fn puts(s: *const u8) -> i32;
}

#[start] // Говорим, что выполнение надо начинать с этого символа
fn main(_argc: isize, _argv: *const *const u8) -> isize {
  unsafe {
    // В Расте строки не нуль-терминированные
    puts("Hello, world!\0".as_ptr());
  }
  return 0;
}

#[panic_handler] // Удовлетворяем компилятор
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
  loop {}
}

#[lang = "eh_personality"] // Удовлетворяем компилятор
extern "C" fn eh_personality() {}

Так как здесь и далее код требует нестабильных фич, советую воткнуть в корень проекта файлик, который будет управлять версией компилятора:

$ cat rust-toolchain.toml 
[toolchain]
channel = "nightly-2022-06-09"

Если такой версии компилятора на компе нет, то cargo вызовет rustup, чтобы тот поставил нужную версию. Если такой компилятор есть, то любые действия с cargo по компиляции будут использовать указанную в конфиге версию.

А в Cargo.toml добавить отключение размотки, все равно она в коде нигде не будет использоваться:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Для этого хэлло ворлда cargo-expand покажет следующее:

#[prelude_import]
use core::prelude::rust_2021::*;
#[macro_use]
extern crate core;
...

То есть компилятор неявно вставил импорт библиотеки core (extern crate core;) и прелюдию core (use core::prelude::rust_2021::*;).

Ниже представлена сводная таблица, описывающая разницу между core и std.


  1. да, если используется крейт alloc и настроен глобальный аллокатор;
  2. да, если коллекции тоже #![no_std] и зависят от core.

Большинство структур и типажей стандартной библиотеки описываются именно в core, а не в std:


  • Методы примитивов bool, i32…;
  • Типы Range, Option, Result, Cell, RefCell, PhantomData…;
  • Типажи Hash, Drop, Debug, Iterator, Future, Unpin…;
  • Функции forget, drop, swap


Отключаем core

Мы не ищем лёгких путей, поэтому мы отключим и std, и core с помощью атрибута #![no_core]. Такая функциональность по разным оценкам требуется от 3577 до 4518 людям в мире на момент написания статьи (именно столько людей контрибутят в компилятор Раста, но github даёт одни цифры, а git log --format="%an" | sort -u | wc -l другие). Вы же не думаете, что я тут беру статистику с потолка?

#![feature(no_core)]
#![feature(lang_items)]

#![no_core]

// Говорим компилятору влинковать libc
#[cfg(target_os = "linux")]
#[link(name = "c")]
extern {}

// Функция `main` на самом деле не точка входа, а вот `start` - да.
#[lang = "start"]
fn start(_main: fn() -> T, _argc: isize, _argv: *const *const u8) -> isize {
  42
}

// Втыкаем символ, чтобы не получить ошибку undefined reference to `main'
fn main() { }

// Нужно компилятору
#[lang = "sized"]
pub trait Sized {}

Проверить работоспособность можно только по коду возврата: echo $? должен вернуть 42.

Мы почти добрались до самого низа. У нас нет возможности складывать числа, если попробовать их сложить, будет ошибка:

error[E0369]: cannot add `{integer}` to `{integer}`
  --> src/main.rs:14:8
   |
14 |     40 + 2
   |     -- ^ - {integer}
   |     |
   |     {integer}

Да ничего у нас нет, только определение примитивов i8, usize, str, но работать с ними нельзя.


Отключаем crt

Rust компилирует объектные файлы самостоятельно, но использует внешний (обычно это системный) линковщик. По умолчанию линковщик добавляет *crt*.o, в которых определяется стартовый символ (_start), но этот символ можно переопределить. Для этого отключаем сишный рантайм:

$ cargo rustc -- -C link-args=-nostartfiles

Или с помощью конфига в корне проекта можно задать флаги линковки:

$ cat .cargo/config 
[build]
rustflags = ["-C", "link-args=-nostartfiles"]

Тогда с .cargo/config и rust-toolchain.toml файлом сборка проекта осуществляется короткой командой cargo build. Ну или вы можете вбивать cargo +nightly-2022-06-09 rustc -- -C link-args=-nostartfiles.

Вид нашего хэлло ворлда приобретает форму:

#![feature(no_core)]
#![feature(lang_items)]
#![no_core]
#![no_main]

#[no_mangle]
extern "C" fn _start() {}

// Нужно компилятору
#[lang = "sized"]
pub trait Sized {}

Девственный ассемблер:

$ objdump -Cd ./target/debug/hello_world

./target/debug/hello_world:     file format elf64-x86-64

Disassembly of section .text:

0000000000001000 <_start>:
    1000:   c3                      retq

Компилируем и запускаем:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/hello_world`
Illegal instruction (core dumped)

Прекрасно. С этим можно начинать работать.

Вообще до мейна происходит очень много интересного: инициализация статиков, профилировщика. Советую посмотреть доклад Мэтта Годболта:

https://www.youtube.com/watch? v=dOfucXtyEsU

Мы же напишем простой _start с прыжком в _start_main, который и будет вызывать функцию main. Подложка в виде _start_main нужна, чтобы можно было положиться на компилятор в вопросах передачи аргументов и очистки стека.


Символ _start

Его мы будем писать на ассемблере. В std/core препроцессор ассемблерных вставок включается по умолчанию, а вот нам надо включить его явно.

#![feature(decl_macro)]
#![feature(rustc_attrs)]
#[rustc_builtin_macro]
pub macro asm("assembly template", $(operands,)* $(options($(option),*))?) {
  /* compiler built-in */
}

_start — это специальная функция, которой не требуется пролог и эпилог, поэтому ее надо пометить как naked.

#![feature(naked_functions)]

#[no_mangle]
#[naked]
unsafe extern "C" fn _start() {
  // Стырено из книги А.В. Столярова.
  // А, простите, там код под 32 бита, в книге 2021 года.
  // Значит, не стырено.
  asm!(
    "mov rdi, [rsp]", // argc
    "mov rax, rsp",
    "add rax, 8",
    "mov rsi, rax", // argv
    "call _start_main",
    options(noreturn),
  )
}

#[no_mangle]
extern "C" fn _start_main(argc: usize, argv: *const *const u8) -> isize {
  main(argc, argv);
  0
}

#[no_mangle]
fn main(_argc: usize, _argv: *const *const u8) -> isize {
  // И вот мы добрались до мейна
  return 0;
}

Компилируем и запускаем: Illegal instruction (core dumped). Я чую, что мы на правильном пути!


Сисколы

Всего нам понадобится два сискола: exit и write.

«Подложки» для сисколлов я хочу реализовать в общем виде, чтобы они принимали номер сисколла и аргументы (syscall1 — 1 аргумент, syscall3 — 3 аргумента).

man 2 syscall дает нам следующую информацию:


Architecture calling conventions
Every  architecture has its own way of invoking and passing arguments
to the kernel.  The details for various architectures are listed
in the two tables below.

The first table lists the instruction used to transition to kernel mode
(which might not be the fastest or best way to transition to the kernel,
so  you might have to refer to vdso(7)), the register used to indicate
the system call number, the register(s) used to return the system call
result, and the register used to signal an error.

Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
                                  call #  val  val2
───────────────────────────────────────────────────────────────────
i386        int $0x80             eax     eax  edx  -
x86-64      syscall               rax     rax  rdx  -        5

The second table shows the registers used to pass the system call arguments.

Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
i386          ebx   ecx   edx   esi   edi   ebp   -
x86-64        rdi   rsi   rdx   r10   r8    r9    -


Завершение процесса

У данного системного вызова есть замечательное свойство — он никогда не возвращается. Этот факт можно использовать с помощью типов и интринзиков, чтобы дать понять компилятору, что любой код после данного сискола никогда не будет выполнен. Это реализуется через тип ! (never) и интринзик unreachable:

#![feature(intrinsics)] // подключаем фичу объявления интринзиков

extern "rust-intrinsic" {
  // Чтобы компилятор знал, что есть некоторый код, которого не достичь.
  // Например, весь код после exit()
  pub fn unreachable() -> !;
}

#[no_mangle]
extern "C" fn _start_main(argc: usize, argv: *const *const u8) -> ! {
  let status = main(argc, argv);
  exit(status);
}

#[inline(never)]
#[no_mangle]
// ! - это never type, компилятор понимает, что функция никогда не возвращается
fn exit(exit_code: i64) -> ! {
  unsafe {
    syscall1(60, exit_code);
    unreachable()
  }
}

#[inline(always)]
unsafe fn syscall1(n: i64, a1: i64) -> i64 {
  let ret: i64;
  asm!(
    "syscall",
    in("rax") n,
    in("rdi") a1,
    lateout("rax") ret,
  );
  ret
}

Если запустить получившийся бинарник, echo $? вернет ожидаемый 0.


Запись в файл

Настало время реализовать вывод «Hello, world!» в стандартный поток вывода! \<Не забыть изменить на менее глупую фразу перед публикацией>.

#[no_mangle]
fn main(_argc: usize, _argv: *const *const u8) -> i64 {
  let string = b"Hello, world!\n" as *const _ as *const u8;
  write(1, string, 14);
  return 0;
}

#[inline(never)]
#[no_mangle]
fn write(fd: i64, data: *const u8, len: i64) -> i64 {
  unsafe { syscall3(1, fd, data as i64, len) }
}

#[inline(always)]
unsafe fn syscall3(n: i64, a1: i64, a2: i64, a3: i64) -> i64 {
  let ret: i64;
  asm!(
    "syscall",
    in("rax") n,
    in("rdi") a1,
    in("rsi") a2,
    in("rdx") a3,
    lateout("rax") ret,
  );
  ret
}


Хелло ворлд на Расте под Linux x86_64 целиком
#![feature(no_core)]
#![feature(lang_items)]
#![no_core]
#![no_main]
#![feature(naked_functions)]
#![feature(decl_macro)]
#![feature(rustc_attrs)]
#![feature(intrinsics)]

// Нужно компилятору
#[lang = "sized"]
pub trait Sized {}

#[lang = "copy"]
pub trait Copy {}

impl Copy for i64 {} // Говорим компилятору, что объект этого типа можно копировать байт за байтом
impl Copy for usize {}

#[rustc_builtin_macro]
pub macro asm("assembly template", $(operands,)* $(options($(option),*))?) {
  /* compiler built-in */
}

extern "rust-intrinsic" {
  // Чтобы компилятор знал, что есть некоторый код, которого не достичь.
  // Например, весь код после exit()
  pub fn unreachable() -> !;
}

#[no_mangle]
#[naked]
unsafe extern "C" fn _start() {
  // Стырено из книги А.В. Столярова.
  // А, простите, там код под 32 бита, в книге 2021 года.
  // Значит, не стырено.
  asm!(
    "mov rdi, [rsp]", // argc
    "mov rax, rsp",
    "add rax, 8",
    "mov rsi, rax", // argv
    "call _start_main",
    options(noreturn),
  )
}

#[no_mangle]
extern "C" fn _start_main(argc: usize, argv: *const *const u8) -> ! {
  let status = main(argc, argv);
  exit(status);
}

#[no_mangle]
fn main(_argc: usize, _argv: *const *const u8) -> i64 {
  let string = b"Hello, world!\n" as *const _ as *const u8;
  write(1, string, 14);
  return 0;
}

#[inline(never)]
#[no_mangle]
// ! - это never type, компилятор понимает, что функция никогда не возвращается
fn exit(status: i64) -> ! {
  unsafe {
    syscall1(60, status);
    unreachable()
  }
}

#[inline(never)]
#[no_mangle]
fn write(fd: i64, data: *const u8, len: i64) -> i64 {
  unsafe { syscall3(1, fd, data as i64, len) }
}

#[inline(always)]
unsafe fn syscall1(n: i64, a1: i64) -> i64 {
  let ret: i64;
  asm!(
    "syscall",
    in("rax") n,
    in("rdi") a1,
    lateout("rax") ret,
  );
  ret
}

#[inline(always)]
unsafe fn syscall3(n: i64, a1: i64, a2: i64, a3: i64) -> i64 {
  let ret: i64;
  asm!(
    "syscall",
    in("rax") n,
    in("rdi") a1,
    in("rsi") a2,
    in("rdx") a3,
    lateout("rax") ret,
  );
  ret
}

Запускаем и проверяем:

$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/hello_world`
Hello, world!
$ echo $?
0
$ strip ./target/debug/hello_world
$ stat -c %s ./target/debug/hello_world
13096

Оно работает! Но размер бинарника 13096 байт. Что ж, применим ld.lld-14:

$ cat .cargo/config 
[build]
rustflags = ["-C", "linker=ld.lld-14"]
$ cargo r
   Compiling hello_world v0.1.0 (/home/USER/rustmustdie/article/chapter_4)
    Finished dev [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/hello_world`
Hello, world!
$ echo $?
0
$ strip ./target/debug/hello_world
$ stat -c %s ./target/debug/hello_world
1712

Уии!

То есть нет =(Получилось 1712 байт против 1132 байт сишной реализации. Не забываем, что в сишной реализации вообще другой код, он хитрый, с непростым приветствием, то есть у него больше функциональность, но меньше размер.

Вот было бы здорово, если бы у нас был:


  • Единый компилятор (gcc),
  • Единый линковщик (ld.lld-14),
  • Одни и те же флаги компиляции -Os -masm=intel -m32 -fno-pic -fno-asynchronous-unwind-tables,
  • Одни и те же флаги линковки --no-pie --no-dynamic-linker,
  • Да и код, выполняющий одну и ту же программу, не правда ли?
  • Чтобы был _start с прыжком в _start_main, который и будет вызывать функцию main,
  • Чтобы было два сискола sys_exit и sys_write (именование из книги Столярова),
  • Чтобы они были реализованы через обобщение сисколов syscall1 и syscall3.

Жаль, что все вместе это невозможно… Or is it?


0*K4Phe3JokmDmxWeE

Компилируем gcc и rustc_codegen_gcc

Архитектура компилятора rustc позволяет подключить не только бекенд llvm, но и gcc. Проект, который занимается поддержкой gcc, называется rustc_codegen_gcc. Конечно же не все так просто, с ним надо провести профекалтическую работу.

$ sudo apt install flex make gawk libgmp-dev libmpfr-dev libmpc-dev gcc-multilib

Клонируем rustc_codegen_gcc, патченный gcc и собираем gcc с поддержкой i386:

# У меня версия 1724042e228c3 от Wed Sep 14 09:22:50 2022
$ git clone https://github.com/rust-lang/rustc_codegen_gcc.git --depth 1
rustc_codegen_gcc$ cd rustc_codegen_gcc

#BUILD GCC (20 mins)
rustc_codegen_gcc$ git clone https://github.com/antoyo/gcc.git --depth 1
rustc_codegen_gcc$ cd gcc
rustc_codegen_gcc/gcc$ mkdir build install
rustc_codegen_gcc/gcc$ cd build
rustc_codegen_gcc/gcc/build$ ../configure --enable-host-shared --enable-languages=jit,c --disable-bootstrap --enable-multilib --target=x86_64-pc-linux-gnu --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64 --enable-multiarch --prefix=$(pwd)/../install
rustc_codegen_gcc/gcc/build$ make -j8
rustc_codegen_gcc/gcc/build$ make install # в папочку ../install
rustc_codegen_gcc/gcc/build$ cd ../../
rustc_codegen_gcc$ echo $(pwd)/gcc/install/lib/ > gcc_path

Мастер ветка пока что не поддерживает i386 из коробки, но это можно исправить:


Патч rustc_codegen_gcc, чтобы заработал i386
diff --git a/config.sh b/config.sh
index b25e215..18574f2 100644
--- a/config.sh
+++ b/config.sh
@@ -20,8 +20,9 @@ else
 fi

 HOST_TRIPLE=$(rustc -vV | grep host | cut -d: -f2 | tr -d " ")
-TARGET_TRIPLE=$HOST_TRIPLE
+#TARGET_TRIPLE=$HOST_TRIPLE
 #TARGET_TRIPLE="m68k-unknown-linux-gnu"
+TARGET_TRIPLE="i686-unknown-linux-gnu"

 linker=''
 RUN_WRAPPER=''
@@ -33,6 +34,8 @@ if [[ "$HOST_TRIPLE" != "$TARGET_TRIPLE" ]]; then
       # We are cross-compiling for aarch64. Use the correct linker and run tests in qemu.
       linker='-Clinker=aarch64-linux-gnu-gcc'
       RUN_WRAPPER='qemu-aarch64 -L /usr/aarch64-linux-gnu'
+   elif [[ "$TARGET_TRIPLE" == "i686-unknown-linux-gnu" ]]; then
+      : # do nothing
    else
       echo "Unknown non-native platform"
    fi
diff --git a/src/back/write.rs b/src/back/write.rs
index efcf18d..e640fbe 100644
--- a/src/back/write.rs
+++ b/src/back/write.rs
@@ -14,6 +14,8 @@ pub(crate) unsafe fn codegen(cgcx: &CodegenContext, _diag_han
     let _timer = cgcx.prof.generic_activity_with_arg("LLVM_module_codegen", &*module.name);
     {
         let context = &module.module_llvm.context;
+        context.add_command_line_option("-m32");
+        context.add_driver_option("-m32");

         let module_name = module.name.clone();
         let module_name = Some(&module_name[..]);
diff --git a/src/base.rs b/src/base.rs
index 8cc9581..fb8bd88 100644
--- a/src/base.rs
+++ b/src/base.rs
@@ -98,7 +98,7 @@ pub fn compile_codegen_unit<'tcx>(tcx: TyCtxt<'tcx>, cgu_name: Symbol, supports_
         context.add_command_line_option("-mpclmul");
         context.add_command_line_option("-mfma");
         context.add_command_line_option("-mfma4");
-        context.add_command_line_option("-m64");
+        context.add_command_line_option("-m32");
         context.add_command_line_option("-mbmi");
         context.add_command_line_option("-mgfni");
         context.add_command_line_option("-mavxvnni");
diff --git a/src/context.rs b/src/context.rs
index 2699559..056352a 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -161,13 +161,13 @@ impl<'gcc, 'tcx> CodegenCx<'gcc, 'tcx> {
         let ulonglong_type = context.new_c_type(CType::ULongLong);
         let sizet_type = context.new_c_type(CType::SizeT);

-        let isize_type = context.new_c_type(CType::LongLong);
-        let usize_type = context.new_c_type(CType::ULongLong);
+        let isize_type = context.new_c_type(CType::Int);
+        let usize_type = context.new_c_type(CType::UInt);
         let bool_type = context.new_type::();

         // TODO(antoyo): only have those assertions on x86_64.
-        assert_eq!(isize_type.get_size(), i64_type.get_size());
-        assert_eq!(usize_type.get_size(), u64_type.get_size());
+        assert_eq!(isize_type.get_size(), i32_type.get_size());
+        assert_eq!(usize_type.get_size(), u32_type.get_size());

         let mut functions = FxHashMap::default();
         let builtins = [
diff --git a/src/int.rs b/src/int.rs
index 0c5dab0..5fd4925 100644
--- a/src/int.rs
+++ b/src/int.rs
@@ -524,7 +524,7 @@ impl<'a, 'gcc, 'tcx> Builder<'a, 'gcc, 'tcx> {
         // when having proper sized integer types.
         let param_type = bswap.get_param(0).to_rvalue().get_type();
         if param_type != arg_type {
-            arg = self.bitcast(arg, param_type);
+            arg = self.cx.context.new_cast(None, arg, param_type);
         }
         self.cx.context.new_call(None, bswap, &[arg])
     }
diff --git a/src/lib.rs b/src/lib.rs
index e43ee5c..8fb5823 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -104,6 +104,7 @@ impl CodegenBackend for GccCodegenBackend {
         let temp_dir = TempDir::new().expect("cannot create temporary directory");
         let temp_file = temp_dir.into_path().join("result.asm");
         let check_context = Context::default();
+        check_context.add_command_line_option("-m32");
         check_context.set_print_errors_to_stderr(false);
         let _int128_ty = check_context.new_c_type(CType::UInt128t);
         // NOTE: we cannot just call compile() as this would require other files than libgccjit.so.

И поверх этого патча надо применить еще один, чтобы libgccjit.so компилировал только с нужным набором флагов:


Патч rustc_codegen_gcc для унификации флагов
diff --git a/src/base.rs b/src/base.rs
index fb8bd88..d5268dc 100644
--- a/src/base.rs
+++ b/src/base.rs
@@ -87,29 +87,11 @@ pub fn compile_codegen_unit<'tcx>(tcx: TyCtxt<'tcx>, cgu_name: Symbol, supports_
         // Instantiate monomorphizations without filling out definitions yet...
         //let llvm_module = ModuleLlvm::new(tcx, &cgu_name.as_str());
         let context = Context::default();
-        // TODO(antoyo): only set on x86 platforms.
         context.add_command_line_option("-masm=intel");
-        // TODO(antoyo): only add the following cli argument if the feature is supported.
-        context.add_command_line_option("-msse2");
-        context.add_command_line_option("-mavx2");
-        // FIXME(antoyo): the following causes an illegal instruction on vmovdqu64 in std_example on my CPU.
-        // Only add if the CPU supports it.
-        context.add_command_line_option("-msha");
-        context.add_command_line_option("-mpclmul");
-        context.add_command_line_option("-mfma");
-        context.add_command_line_option("-mfma4");
         context.add_command_line_option("-m32");
-        context.add_command_line_option("-mbmi");
-        context.add_command_line_option("-mgfni");
-        context.add_command_line_option("-mavxvnni");
-        context.add_command_line_option("-mf16c");
-        context.add_command_line_option("-maes");
-        context.add_command_line_option("-mxsavec");
-        context.add_command_line_option("-mbmi2");
-        context.add_command_line_option("-mrtm");
-        context.add_command_line_option("-mvaes");
-        context.add_command_line_option("-mvpclmulqdq");
-        context.add_command_line_option("-mavx");
+        context.add_command_line_option("-fno-pic");
+        context.add_command_line_option("-fno-asynchronous-unwind-tables");
+        context.add_command_line_option("-Os");

         for arg in &tcx.sess.opts.cg.llvm_args {
             context.add_command_line_option(arg);

Клонируем llvm и собираем rustc_codegen_gcc:

#BUILD RUSTC: (5 mins)
rustc_codegen_gcc$ git clone https://github.com/llvm/llvm-project llvm --depth 1 --single-branch
rustc_codegen_gcc$ export RUST_COMPILER_RT_ROOT="$PWD/llvm/compiler-rt"
rustc_codegen_gcc$ ./prepare_build.sh # download and patch sysroot src
rustc_codegen_gcc$ ./build.sh

Всё, теперь у нас есть собранный своими ручками компилятор Си (~/rustc_codegen_gcc/gcc/install/bin/gcc), libgccjit.so для компиляции Раста c захардкоженными флагами -Os -masm=intel -m32 -fno-pic -fno-asynchronous-unwind-tables и скрипт ~/rustc_codegen_gcc/cargo.sh, который подсовывает фронтенду rustc бекенд gcc.


Хэлло ворлд на Си под i386


Код
int sys_write(int fd, const void *buf, int size);
void sys_exit(int status);
static int main(int argc, char **argv);
static int syscall1(int n, int a1);
static int syscall3(int n, int a1, int a2, int a3);

static const char hello[] = "Hello, world!\n";

void _Noreturn __attribute__((naked)) _start() {
  __asm volatile (
    "_start:\n"
    "  mov ecx, [esp]\n"
    "  mov eax, esp\n"
    "  add eax, 4\n"
    "  push eax\n"
    "  push ecx\n"
    "  call _start_main\n"
  );
}

void _Noreturn _start_main(int argc, char **argv) {
  int status = main(argc, argv);
  sys_exit(status);
}

static int main(int argc, char **argv)
{
  sys_write(1, hello, sizeof(hello)-1);
  return 0;
}

void _Noreturn __attribute__ ((noinline)) sys_exit(int status) {
  syscall1(1, status);
  __builtin_unreachable();
}

int __attribute__ ((noinline)) sys_write(int fd, const void *buf, int size) {
  return syscall3(4, fd, (int) buf, size);
}

static int syscall1(int n, int a1) {
  int ret;
  __asm volatile (
    "  int 0x80"
    : "=a" (ret)
    : "0" (n), "b" (a1)
    : "memory"
  );
  return ret;
}

static int syscall3(int n, int a1, int a2, int a3) {
  int ret;
  __asm volatile (
    "  int 0x80"
    : "=a" (ret)
    : "0" (n), "b" (a1), "c" (a2), "d" (a3)
    : "memory"
  );
  return ret;
}

Все эти приседания с _Noreturn, static, __attribute__((naked)) прямое отражение того, что было в коде на Расте. Т.е. говорим компилятору, что из sys_exit нельзя выйти, static — для красивого инлайна (и чтобы в итоговом бинаре отсутствовал такой символ), а __attribute__((naked)) — чтобы компилятор не вставил пролог и эпилог для _start.

Сборка:

~/rustc_codegen_gcc/gcc/install/bin/gcc -Os -masm=intel -m32 -fno-pic -fno-asynchronous-unwind-tables -Wall -Wno-main -c hello_world.c
ld.lld-14 --no-pie --no-dynamic-linker hello_world.o -o hello_world
strip hello_world
objcopy -j.text -j.rodata hello_world

Проверяем:

$ ./build.sh
$ ./hello_world 
Hello, world!


Хэлло ворлд на Расте под i386


Код
#![feature(no_core)]
#![feature(lang_items)]
#![feature(naked_functions)]
#![feature(decl_macro)]
#![feature(rustc_attrs)]
#![feature(intrinsics)]
#![no_core]
#![no_main]

#[lang = "sized"]
pub trait Sized {}

#[lang = "copy"]
pub trait Copy {}

impl Copy for i32 {}
impl Copy for usize {}

#[rustc_builtin_macro]
pub macro asm("assembly template", $(operands,)* $(options($(option),*))?) {
  /* compiler built-in */
}

extern "rust-intrinsic" {
  pub fn unreachable() -> !;
}

#[no_mangle]
#[naked]
unsafe extern "C" fn _start() {
  asm!(
    "mov ecx, [esp]",
    "mov eax, esp",
    "add eax, 4",
    "push eax",
    "push ecx",
    "call _start_main",
    options(noreturn),
  )
}

#[no_mangle]
extern "C" fn _start_main(argc: usize, argv: *const *const u8) -> ! {
  let status = main(argc, argv);
  sys_exit(status);
}

#[no_mangle]
fn main(_argc: usize, _argv: *const *const u8) -> i32 {
  let string = b"Hello, world!\n" as *const _ as *const u8;
  sys_write(1, string, 14);
  return 0;
}

#[inline(never)]
#[no_mangle]
fn sys_write(fd: i32, data: *const u8, len: i32) -> i32 {
  unsafe { syscall3(4, fd, data as _, len) }
}

#[inline(never)]
#[no_mangle]
fn sys_exit(status: i32) -> ! {
  unsafe {
    syscall1(1, status);
    unreachable()
  }
}

#[inline(always)]
unsafe extern "C" fn syscall1(n: i32, a1: i32) -> i32 {
  let ret: i32;
  asm!(
    "int 0x80",
    in("eax") n,
    in("ebx") a1,
    lateout("eax") ret,
  );
  ret
}

#[inline(always)]
unsafe fn syscall3(n: i32, a1: i32, a2: i32, a3: i32) -> i32 {
  let ret: i32;
  asm!(
    "int 0x80",
    in("eax") n,
    in("ebx") a1,
    in("ecx") a2,
    in("edx") a3,
    lateout("eax") ret,
  );
  ret
}

Сборка:

# cargo.sh, предоставляемый rustc_codegen_gcc, принимает только переменную окружения CG_RUSTFLAGS
# поэтому в .cargo/config эти переменные не установить. Увы. 
export CG_RUSTFLAGS="-C linker=ld.lld-14 -C link-args=--no-pie -C link-args=--no-dynamic-linker"
~/rustc_codegen_gcc/cargo.sh b --target i686-unknown-linux-gnu
strip ./target/i686-unknown-linux-gnu/debug/hello_world
objcopy -j.text -j.rodata ./target/i686-unknown-linux-gnu/debug/hello_world

Проверяем:

$ ./build.sh 
rustc_codegen_gcc is build for rustc 1.65.0-nightly (748038961 2022-08-25) but the default rustc version is rustc 1.63.0-nightly (7466d5492 2022-06-08).
Using rustc 1.65.0-nightly (748038961 2022-08-25).
   Compiling hello_world v0.1.0 (/home/USER/rustmustdie/article/chapter_6)
    Finished dev [unoptimized + debuginfo] target(s) in 0.84s
$ ./target/i686-unknown-linux-gnu/debug/hello_world 
Hello, world!


Сравнение

Вот так, размер файла на Расте получился 464 байта, а на Си — 494 байт. Предлагаю читателю самостоятельно ответить на вопрос, обладает ли Раст свойством абсолютной независимости от библиотечного кода, также иногда называемым zero runtime.

Для интересующихся, вот вся инфа о бинарях:


Си
$ objdump -Cd hello_world

hello_world:     file format elf32-i386

Disassembly of section .text:

004010e3 <_start>:
  4010e3:   8b 0c 24                mov    (%esp),%ecx
  4010e6:   89 e0                   mov    %esp,%eax
  4010e8:   83 c0 04                add    $0x4,%eax
  4010eb:   50                      push   %eax
  4010ec:   51                      push   %ecx
  4010ed:   e8 27 00 00 00          call   401119 <_start_main>
  4010f2:   0f 0b                   ud2    

004010f4 :
  4010f4:   55                      push   %ebp
  4010f5:   b8 01 00 00 00          mov    $0x1,%eax
  4010fa:   89 e5                   mov    %esp,%ebp
  4010fc:   53                      push   %ebx
  4010fd:   8b 5d 08                mov    0x8(%ebp),%ebx
  401100:   cd 80                   int    $0x80

00401102 :
  401102:   55                      push   %ebp
  401103:   b8 04 00 00 00          mov    $0x4,%eax
  401108:   89 e5                   mov    %esp,%ebp
  40110a:   53                      push   %ebx
  40110b:   8b 4d 0c                mov    0xc(%ebp),%ecx
  40110e:   8b 55 10                mov    0x10(%ebp),%edx
  401111:   8b 5d 08                mov    0x8(%ebp),%ebx
  401114:   cd 80                   int    $0x80
  401116:   5b                      pop    %ebx
  401117:   5d                      pop    %ebp
  401118:   c3                      ret    

00401119 <_start_main>:
  401119:   55                      push   %ebp
  40111a:   89 e5                   mov    %esp,%ebp
  40111c:   83 ec 0c                sub    $0xc,%esp
  40111f:   6a 0e                   push   $0xe
  401121:   68 d4 00 40 00          push   $0x4000d4
  401126:   6a 01                   push   $0x1
  401128:   e8 d5 ff ff ff          call   401102 
  40112d:   31 c0                   xor    %eax,%eax
  40112f:   89 04 24                mov    %eax,(%esp)
  401132:   e8 bd ff ff ff          call   4010f4 

$ readelf -a hello_world
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2s complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x4010e3
  Start of program headers:          52 (bytes into file)
  Start of section headers:          336 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         4
  Size of section headers:           40 (bytes)
  Number of section headers:         4
  Section header string table index: 3

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .rodata           PROGBITS        004000d4 0000d4 00000f 00   A  0   0  4
  [ 2] .text             PROGBITS        004010e3 0000e3 000054 00  AX  0   0  1
  [ 3] .shstrtab         STRTAB          00000000 000137 000019 00      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),
  p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00400034 0x00400034 0x00080 0x00080 R   0x4
  LOAD           0x000000 0x00400000 0x00400000 0x000e3 0x000e3 R   0x1000
  LOAD           0x0000e3 0x004010e3 0x004010e3 0x00054 0x00054 R E 0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .rodata 
   02     .text 
   03     

$ objdump -s ./hello_world

./hello_world:     file format elf32-i386

Contents of section .rodata:
 4000d4 48656c6c 6f2c2077 6f726c64 210a00    Hello, world!.. 
Contents of section .text:
 4010e3 8b0c2489 e083c004 5051e827 0000000f  ..$.....PQ.''....
 4010f3 0b55b801 00000089 e5538b5d 08cd8055  .U.......S.]...U
 401103 b8040000 0089e553 8b4d0c8b 55108b5d  .......S.M..U..]
 401113 08cd805b 5dc35589 e583ec0c 6a0e68d4  ...[].U.....j.h.
 401123 0040006a 01e8d5ff ffff31c0 890424e8  .@.j......1...$.
 401133 bdffffff                             ....               


Раст
$ objdump -Cd ./target/i686-unknown-linux-gnu/debug/hello_world

./target/i686-unknown-linux-gnu/debug/hello_world:     file format elf32-i386

Disassembly of section .text:

004010c2 <_start>:
  4010c2:   8b 0c 24                mov    (%esp),%ecx
  4010c5:   89 e0                   mov    %esp,%eax
  4010c7:   83 c0 04                add    $0x4,%eax
  4010ca:   50                      push   %eax
  4010cb:   51                      push   %ecx
  4010cc:   e8 25 00 00 00          call   4010f6 <_start_main>

004010d1 :
  4010d1:   55                      push   %ebp
  4010d2:   b8 04 00 00 00          mov    $0x4,%eax
  4010d7:   89 e5                   mov    %esp,%ebp
  4010d9:   53                      push   %ebx
  4010da:   8b 5d 08                mov    0x8(%ebp),%ebx
  4010dd:   8b 4d 0c                mov    0xc(%ebp),%ecx
  4010e0:   8b 55 10                mov    0x10(%ebp),%edx
  4010e3:   cd 80                   int    $0x80
  4010e5:   5b                      pop    %ebx
  4010e6:   5d                      pop    %ebp
  4010e7:   c3                      ret    

004010e8 :
  4010e8:   55                      push   %ebp
  4010e9:   b8 01 00 00 00          mov    $0x1,%eax
  4010ee:   89 e5                   mov    %esp,%ebp
  4010f0:   53                      push   %ebx
  4010f1:   8b 5d 08                mov    0x8(%ebp),%ebx
  4010f4:   cd 80                   int    $0x80

004010f6 <_start_main>:
  4010f6:   55                      push   %ebp
  4010f7:   89 e5                   mov    %esp,%ebp
  4010f9:   83 ec 0c                sub    $0xc,%esp
  4010fc:   6a 0e                   push   $0xe
  4010fe:   68 b4 00 40 00          push   $0x4000b4
  401103:   6a 01                   push   $0x1
  401105:   e8 c7 ff ff ff          call   4010d1 
  40110a:   31 c0                   xor    %eax,%eax
  40110c:   89 04 24                mov    %eax,(%esp)
  40110f:   e8 d4 ff ff ff          call   4010e8 

$ readelf -a ./target/i686-unknown-linux-gnu/debug/hello_world
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2s complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x4010c2
  Start of program headers:          52 (bytes into file)
  Start of section headers:          304 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         4
  Size of section headers:           40 (bytes)
  Number of section headers:         4
  Section header string table index: 3

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .rodata           PROGBITS        004000b4 0000b4 00000e 00   A  0   0  4
  [ 2] .text             PROGBITS        004010c2 0000c2 000052 00  AX  0   0  1
  [ 3] .shstrtab         STRTAB          00000000 000114 000019 00      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),
  p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00400034 0x00400034 0x00080 0x00080 R   0x4
  LOAD           0x000000 0x00400000 0x00400000 0x000c2 0x000c2 R   0x1000
  LOAD           0x0000c2 0x004010c2 0x004010c2 0x00052 0x00052 R E 0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .rodata 
   02     .text 
   03     

$ objdump -s ./target/i686-unknown-linux-gnu/debug/hello_world

./target/i686-unknown-linux-gnu/debug/hello_world:     file format elf32-i386

Contents of section .rodata:
 4000b4 48656c6c 6f2c2077 6f726c64 210a      Hello, world!.  
Contents of section .text:
 4010c2 8b0c2489 e083c004 5051e825 00000055  ..$.....PQ.%...U
 4010d2 b8040000 0089e553 8b5d088b 4d0c8b55  .......S.]..M..U
 4010e2 10cd805b 5dc355b8 01000000 89e5538b  ...[].U.......S.
 4010f2 5d08cd80 5589e583 ec0c6a0e 68b40040  ]...U.....j.h..@
 401102 006a01e8 c7ffffff 31c08904 24e8d4ff  .j......1...$...
 401112 ffff                                 ..              

Сишная версия толще на одну инструкцию ud2 (занимает 2 байта) и на один нуль в конце строки. В sys_exit аргументы пушатся в разном порядке, а в бинарях в целом символы находятся по разным адресам, а так бинари абсолютно идентичны.

Полученный результат стал возможен благодаря автору rustc_codegen_gcc — Antoyo. Он ведет блог, в котором периодически репортит о прогрессе данного проекта. И прогресс действительно поражает воображение. Пользуясь моментом, я прошу вас запатреонить Antoyo или проспонсировать его на гитхабе. Он делает важное дело не только для языка Раст, но и для проекта gcc (улучшает libgccjit.so), что позволит в будущем отвязаться от llvm и, например, компилировать модули ядра Линукса под все доступные gcc платформы.


Именно это свойство — zero runtime — делает Си единственным и безальтернативным кандидатом на роль языка для реализации ядер операционных систем и прошивок для микроконтроллеров. Тем удивительнее, насколько мало людей в мире этот момент осознают; и стократ удивительнее то, что людей, понимающих это, судя по всему, вообще нет среди членов комитетов по стандартизации (языка Си)…

— А.В. Столяров

Спасибо, буду знать.

Данная статья иллюстрирует простую мысль: если сильно упороться, можно на любом языке писать как на Си. Но надо ли? Эффективность работы программиста во многом зависит от адекватности используемых изобразительных средств по отношению к используемой задаче. А Раст предоставляет широчайшие возможности как по оптимизации кода, так и по минимизации ошибок, возникающих из-за человеческого фактора.

Вся эта история с доцентом, студентом МГУ и статьёй https://rustmustdie.com/ показывает, что где-то внутри вуза построен странный образовательный процесс, который мешает студентам получать актуальную информацию и формировать независимое мнение.

Я бы хотел, чтобы в МГУ (самом МГУ!) ученые и студенты были открыты к познанию. Ведь в этом и есть суть университетов, нет? Слишком многого хочу?…

Весь код из примеров, как и патчи, доступен в репозитории на гитхабе. Проверяйте, перепроверяйте.

Если у вас возник вопрос, а как же реализовать сложение чисел в среде без рантайма, вот ссылка на Linux x86_64 проект с минимальной реализацией арифметики, адресной арифметики, базовых операций вроде взятия размера слайса, ссылки на данные толстого указателя и т.д., благодаря чему в хэлло ворлде вычисляется размер строки, а не подставляется магическое число:

#[no_mangle]
fn main() {
  print("Hello world!\n");
}

fn print(string: &str) {
  unsafe {
    write(1, string.as_ptr(), string.len())
  };
}

© Habrahabr.ru