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.

Функциональность core std
динамическое выделение памяти нет *1 да
коллекции (Vec, HashMap и т.д.) нет *2 да
доступ к std нет да
доступ к core да да
низкоуровневая разработка да нет

  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<T>(_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?


Компилируем 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<GccCodegenBackend>, _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::<bool>();           // 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!

Сравнение

Си Раст
 $ stat -c %s hello_world 496 $ size -A hello_world hello_world  : section   size      addr .rodata     15   4194516 .text       84   4198627 Total       99 
 $ stat -c %s ./hello_world 464 $ size -A ./hello_world ./hello_world  : section   size      addr .rodata     14   4194484 .text       82   4198594 Total       96 

Вот так, размер файла на Расте получился 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 <sys_exit>:   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 <sys_write>:   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 <sys_write>   40112d:   31 c0                   xor    %eax,%eax   40112f:   89 04 24                mov    %eax,(%esp)   401132:   e8 bd ff ff ff          call   4010f4 <sys_exit>  $ 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 <sys_write>:   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 <sys_exit>:   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 <sys_write>   40110a:   31 c0                   xor    %eax,%eax   40110c:   89 04 24                mov    %eax,(%esp)   40110f:   e8 d4 ff ff ff          call   4010e8 <sys_exit>  $ 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())   }; }


ссылка на оригинал статьи https://habr.com/ru/post/598219/


Комментарии

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

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