В предыдущих сериях:
Медленно, но верно Раст проникает не только в умы сотрудников больших корпораций, но и в умы школьников и студентов. В этот раз мы поговорим о статье от студента МГУ: https://rustmustdie.com/.
Её репостнул Андрей Викторович Столяров, доцент кафедры алгоритмических языков факультета ВМК МГУ им. М. В. Ломоносова и по совместительству научрук студента-автора статьи.
Я бы сказал, что тут дело даже не в том, что он "неинтуитивный". Дело скорее в том, что компилятор раста сам решает, когда владение "должно" (с его, компилятора, точки зрения) перейти от одного игрока к другому. А решать это вообще-то должен программист, а не компилятор. Ну и начинается пляска вида "как заставить тупой компайлер сделать то, чего я хочу".
Бред это всё.
— А. В. Столяров
Сама статья короткая, но постулирует довольно большой список спорных утверждений, а именно:
- Стандартная библиотека неотделима от языка
- У него отсутствует нулевой рантайм
- В Rust встроен сборщик мусора
- Компилятор генерирует медленный машинный код
На самом деле набросов еще больше, но достаточно и этого списка.
К сожалению, для опровержения этих пунктов мне придется писать максимально уродские хэлло ворлды, которые только можно представить.
Содержание
Опускаемся на самый низ
Нулевой рантайм в Си
Честно говоря, до прочтения статьи я ни разу не встречал такого определения как zero runtime. Немного погуглив, я наткнулся на книгу А. В. Столярова ISBN 978-5-317-06575-7 Программирование: введение в профессию. II: Системы и сети, изданной в 2021 году. В главе "§4.12: (*) Программа на Си без стандартной библиотеки" приводится определение нулевого рантайма и пример программы.
Реализация подпрограммы _start
(под Linux i386):
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
Модуль с "обертками" для системных вызовов:
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):
/* 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 |
да | да |
низкоуровневая разработка | да | нет |
- да, если используется крейт
alloc
и настроен глобальный аллокатор; - да, если коллекции тоже
#![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
дает нам следующую информацию:
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
}
#![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
из коробки, но это можно исправить:
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
компилировал только с нужным набором флагов:
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/ показывает, что где-то внутри вуза построен странный образовательный процесс, который мешает студентам получать актуальную информацию и формировать независимое мнение.
Я бы мог пройтись практически по всем пунктам студента: толстые бинари, медленный код, сборщик мусора, что времена жизни — это хак компилятора, что заимствования слабее указателей. Но на примере опровержения одного лишь пункта про zero runtime мы видим, как просто набрасывать неправду, и как затратно её опровергать. Этой статьи не сущестовало бы, если бы к образовательному процессу подходили немного с другой стороны, без фанатизма любви к старому, без несокрушимых догм.
Я бы хотел, чтобы в МГУ (самом МГУ!) ученые и студенты были открыты к познанию. Ведь в этом и есть суть университетов, нет? Слишком многого хочу?..
Ссылки на код
Весь код из примеров, как и патчи, доступен в репозитории на гитхабе. Проверяйте, перепроверяйте.
Если у вас возник вопрос, а как же реализовать сложение чисел в среде без рантайма, вот ссылка на Linux x86_64 проект с минимальной реализацией арифметики, адресной арифметики, базовых операций вроде взятия размера слайса, ссылки на данные толстого указателя и т.д., благодаря чему в хэлло ворлде вычисляется размер строки, а не подставляется магическое число:
#[no_mangle]
fn main() {
print("Hello world!\n");
}
fn print(string: &str) {
unsafe {
write(1, string.as_ptr(), string.len())
};
}