Rust machine код

Обновлено: 30.06.2024

I've recently been looking at the Rust programming language. How does it work? Rust code seems to be compiled into ELF or PE (etc) binaries, but I've not been able to find any information on how that's done? Is it compiled to an intermediate format then compiled the rest of the way with gxx for example? Any help (or links) would be really appreciated.

103k 90 90 gold badges 278 278 silver badges 360 360 bronze badges asked Apr 13 '17 at 6:34 Dllewellyn Dllewellyn 603 1 1 gold badge 6 6 silver badges 8 8 bronze badges The Rust compiler uses LLVM as backend, like other compilers do (clang, Swift and others). Does that answer your question? It is unclear what you are asking Apr 13 '17 at 6:38 @aochagavia I think it is very clear what he is asking. Great question helped me a lot. Jun 27 '19 at 15:11

Шак третий: rustc-interface

Ага. Тут мы уже ближе к самому процессу компиляции. Все конфиги подъедены, файлы тоже замеплены. Смотрим на исходники интерфейса. Их хоть и не так-то много, но это наш центральный вокзал, где куча других крейтов собирается воедино.

Так, осматриваемся и находим

Кстати тут же, недалеко, мы можем найти настройку механизма кодогенерации.

Быстренько посмотрим на наши сорцы и увидим что у нас прямо в сорцах есть 3 различных модуля кодогенерации. Что они делают? Превращают MIR в конечный код для системы компиляции. Открываем rustc-codegen-llvm и смотрим в README:

Ок, ну тут всё понятно, мы берём MIR и переделываем его в LLVM IR. После этого LLVM может скомпилировать код в конечный бинарник. Но погодите, помимо LLVM бекенда у нас есть ещё два других! Смотрим туда. rustc-codegen-ssa согласно документации, позволяет генерировать низкоуровневый код, который не будет привязан к определённому бекэнду (например, LLVM) и позволит в дальнейшем использовать другие системы компиляции.

Собственно говоря, прямо там же вы найдёте rustc-codegen-cranelift. То есть MIR в будущем может компилироваться через cranelift, который в идеале ускорит процесс компиляции. Ну это в будущем, пока что проект в процессе тестирования и работает не лучше, чем Газель без мотора.

Открываем модуль и смотрим, что происходит внутри:

Ага, вот тут мы берём быка за рога и начинаем разбирать исходный код на части. Далее, создаём и проверяем AST

И вот тут у нас начинается полное мясо. Прикол вот в чём, обычно компиляторы делают достаточно простую вещь - берёшь сорцы, проходишься по ним несколько раз, парсишь синтаксис, находишь ошибки, разбираешь на куски макросы, всё хорошо. rust в стародавние времена начинал именно так же. Но, со временем, была предложена новая модель компиляции. Запросы. Вместо того чтобы делать всю работу несколько раз подряд, давайте просто превратим проходы в запросы. Результат запроса можно сохранить в кеш. Если пользователь (программист) не менял ничего в определённом файле, то и компилировать его не надо. Если мы видим что делать это не надо, мы просто возвращаем данные из кеша запроса.

Даже если вы и поменяли что-то в каком-либо файле, то благодаря системе запросов вы сможете избежать ненужной перекомпиляции. Что если вы изменили только одну линию в комментариях к файлу? Пересобирать такой не придётся.

Давайте посмотрим на запросы, которые создаёт компилятор:

Парсинг, создание крейта, сбор HIR - всё это делается через запросы. Один момент про который полезно знать, это то что ещё не всё переписано на запросах.

В итоге у нас на выходе получается большая и толстая структура:

И как раз её можно дёргать для выполнения необходимых запросов.

Напоследок

Понятно? Ну и хорошо.

Шаг второй: rustc-driver

Ладно, всё выглядит слишком уж просто. Погружаемся дальше. rustc тянет за собой rustc-driver. Ныряем туда.

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

Да, в этом крейте файлов не так-то много, но что бы тут не творилось, на самом деле всё сводится к вызову методов в крейте под названием interface. Вышеприведённый код это и показывает. interface::run_compiler и поехали.

Что же произошло в rustc-driver? Мы собрали все конфиги. Подгрузили все файлы и нашли их местоположение в файловой системе. Создали замыкание, которое следит за процессом компиляции и запускает линкер после успешной компиляции. Запустили линтеры (если такие имелись) и приготовили сам компилятор к запуску. Давайте запускать.

Шаг девятый: Проверка заимствования

Самая "страшная" функция rust это всем известный borrow cheker. Сам он живёт в

В rust-master\compiler\rustc_mir\src\borrow_check\mod.rs . Да, сам модуль такой же огромный и страшный, как и borrow checker. А вот тут, например, можно найти всю логику проверки заимствования при перемещении переменных rust-master\compiler\rustc_mir\src\borrow_check\diagnostics\move_errors.rs

Шаг восьмой: rustc_mir и rustc_mir_build

Теперь наш HIR можно преобразовать в MIR. Берём ранее созданный TyCtxt и начинаем преобразовывать его в

И так далее по всем нодам. MIR это намного более генерализированная версия HIR. Она очень близка к тому что требует от нас LLVM для компиляции. В результате этой генерализации мы можем намного более эффективно работать над оптимизацией написанного вами кода и заниматься проверками заимствований и оптимизацией.

Постскриптум

А почему бинарник такой-то большой? Ну, на этот вопрос можно ответить легко. Залезаем вашим любимым дебаггером в .pdb файл и смотрим на указатели функций, которые тянутся в наш бинарник. Их много. Подсистема работы со строками, макросами и памятью. Плюс система ввода-вывода. Что, считаете что 150 килобайт - слишком много?

Пробуем ручками

Ну что же, напоследок осталось написать простенькую программку, типа этого:

И начать её компилировать, только показывая все внутренности. Для начала есть замечательная опция компилятора, которая работает на любой версии:

Значит, запуская компиляцию следующим образом:

Мы получаем на выходе мириады различных форматов, включая сгенерированный ассемблеровский код, байткод и IR для LLVM, и даже челвоеко-читаемый MIR.

А если у вас есть nightly компилятор, то вы можете запустить

И полюбоваться вашим HIR, в то время как

Даст вам возможность посмотреть на то, как выглядит AST.

1 Answer 1

The code-generation phase of the Rust compiler is mainly done by LLVM. LLVM is a set of tools for building a compiler, most notably used by the C[++] Compiler clang[++] .

First, the Rust compiler (just like clang , for example) does all the Rust specific stuff like type and borrow checking; in the end, it generates LLVM-IR. IR stands for intermediate representation and it's. comparable to assembly, but a tiny bit more high level and most importantly: platform independent. Then the Rust compiler just calls ☏ LLVM and says:

Hey buddy, could you please take this IR and generate machine code for the current platform? That would be fantastic ◕ ◡ ◕

To which LLVM responds:

🌈 Sure, no problem, new friend. Here is your highly optimized machine code for [e.g.] x86_64 ! ♫♪♫

Afterwards they invite a few more friends to wrap it all up in a nice little [e.g.] ELF package and beautifully place it in the users file system. (and the user is like. )

Information like this can be found in the official FAQ which contains a lot of interesting information anyway. For more in-depth details on the Rust compiler, you can read the "Rustc Guide". For this question, the chapter "High Level Overview" is pretty interesting.


В моей предыдущей статье о rust я попытался рассказать об истории языка, и показать откуда он пришёл. В статье было сделано множество упрощений. Просто нереальное множество. Народу не понравилось. Но в опросе, в конце статьи вы сказали, что надо бы показать кишки компилятора. Ну что же, под катом вы найдёте разбор исходных кодов компилятора rust. Мы проследим путь программы, начиная из исходного файла, прямиком к бинарнику.

Шаг пятый: rustc-expand

В результате работы парсера мы получаем наш самый великий и могучий AST.

Всё это создаётся огромным макросом astfragments! в \compiler\rustcexpand\src\expand.rs

AST используется для дальнейшей генерации кода и приведения его в нужный вид. Про это можно писать отдельную книгу. Но мы пока удовольствуемся там, что AST можно разобрать до HIR.

Начало

Поехали. Мы будем лезть нашими ручками в сам компилятор и смотреть на его исходники. Для начала нам понадобятся кое-какие инструменты. Ставим чистую виртуальную машину с Windows 10. Идём в интернеты и льём следующее:

Сорцы компилятора. Достаются с github. Можно лить просто zip, ибо обратно коммитить мы ничего не будем.

Установщик компилятора. Любая свежая стабильная версия подойдёт.

Не будем мучиться, давайте, заодно, установим nightly компилятор.
rustup toolchain install nightly --allow-downgrade --profile minimal --component clippy rustup default nightly

Guide to Rustc Development. Инструкция по разработке компилятора. 460 страниц. Не хило. Сохраняем pdf.

Ну и хорошо. Этого, для начала достаточно. Отключаемся от проводного интернета, хватаем ноутбук и идём на веранду, сидеть и погружаться. Начинаем погружаться, понимаем что будет глупо говорить о компиляторе, если мы не скомпилируем хоть что-то. Ок, так и сделаем.

Ок, это было просто. Но мы не будем использовать cargo для самой компиляции. Используем компилятор напрямую. Но я же на надо cargo издеваюсь, так ведь?

Отступление по теме

Как не надо устанавливать rust

Как не надо устанавливать rust

Чего? Так, сам по себе компилятор всё собрал, но ругается на отсутствие линкера. От жеж, зараза. То есть, линкер ему нужен внешний. Ругаемся на компилятор, встаём с удобного кресла и идём обратно, подключаться к проводному интернету, потому что палить 5 гигов установщика Visual Studio Build Tools не хочется на хотспоте.

Билдим всё ещё раз и смотрим.

Ширина и жирина файлов.

Ширина и жирина файлов.

Ах, ты, ржавая банка! Какого чёрта?? Я уже как две недели рассказываю всем обитателям Хабра о том, какой ты прекрасный компилятор, и как хорошо ты собираешь минимальные бинарники, а ты. 150 килобайт исполняемого кода из-за одной только линии текста на экране?

Пытаемся скомпилировать с -C opt-level=3 и получаем то же самое. Что случилось с бинарником? Сейчас на этот вопрос отвечать не будем. Мотаем на Ус и едем дальше.

Копирайт - сдесь же.

Копирайт - сдесь же.

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

Ладно, что мы знаем? Компилятор не работает без внешнего линкера и исходник для вывода одной строки текста раздувается до 150 килобайт. Ну, по крайней мере мы это можем скомпилировать. Давайте пока распакуем исходники компилятора и начнём рыться. (Собирать компилятор я не собираюсь. Если вам очень хочется - это можно сделать, но процесс это долгий и утомительный.)

Шак одиннадцатый: прощай, rust!

Полученный оптимизированный MIR можно теперь переделать в LLVM IR. Поехали. rustc-codegen-llvm создаёт LLVM-IR на базе MIR, который мы сгенерировали на предыдущем этапе. Здесь заканчивается rust и начинается llvm. Хотя, мы ещё не закончили с сорцами компилятора.

Тут можно найти пару интересных моментов, например rust-master\compiler\rustc_codegen_llvm\src\asm.rs содержит код для компилирования ассемблера напрямую из rust. Даже не замечал этого. Смотрим в документацию - есть такая поддержка в этом компиляторе!

Копаемся чуть глубже и находим rustc-target в котором видим различные дополнительные классы для работы с определённым ассемблером.

После того как кодогенерация завершена, мы можем передать IR в сам LLVM. rustc_llvm нам в помощь.

Вот, собственно говоря, и всё, ребята! LLVM за пределами нашей видимости. На моей операционной системе Visual Studio Build Tools берут на себя контроль и перегоняют LLVMIR в обычный бинарник.

Процесс компиляции в rust - это вам не мешки ворочать. Надо проверить неимоверное количество разных вещей. Вот что происходит с вашим кодом:

Он парсится из текста в AST.

AST обрабатывается и оптимизируется в HIR

HIR обрабатывается и оптимизируется в MIR.

MIR делает проверки заимствования и оптимизацию и перегоняется в LLVMIR.

LLVMIR компилируется на конечной платформе.

Шаг десятый: Оптимизации

Про систему оптимизаций в rust можно писать отдельную книгу. Всё аккуратно сложено в rust-master\compiler\rustc_mir\src\transform . LLVM сам по себе не сможет оптимизировать некоторые высокоуровневые примитивы, о которых знает только rust. И вот тут мы как раз и занимаемся оптимизацией этих примитивов.

Шаг четвёртый: rustc-parse и rustc-lexer

Далее по тексту вы найдёте простую логику всех этих запросов. "Простая" логика заключается в вызове крейтов, которые её обрабатывают. Например, rustc-parse. Это крейт, который использует rustc-lexer. Лексер читает строки из файлов и преобразовывает их в очень простые токены. Токены передаются парсеру, который превращает их в Span и продолжает работу с кодом. Основной момент этого Span заключается в том, что к каждому элементу в дереве кода будет добавлена информация о том, в каком конкретно месте этот элемент записан в исходном файле. Когда компилятор будет сообщать об ошибке, вы увидите, где именно эта ошибка произошла.

Основная часть парсера запускается через вызов parse_crate_mod в rustc_parse\src\parser\item.rs . А дальше по тексту вы найдёте невероятное количество проверок синтаксиса, который этот парсер делает. Вот, например:

Шаг шестой: rustc-middle

Куда ты завёл нас? Не видно ни зги! Простите, ребята, не варят мозги. Вернее, мозг начинает вариться. Сложность процесса увеличивается настолько, что просто читая коды дальше ходить страшно. Ладно, обратимся к инструкции для разработчиков - смотрим. Видим что после того как у нас появился AST мы можем заняться приведением его в приличный вид. Вернее, в HIR.

Этим как раз и занимается rustc-middle. Вернее, не только этим. Залезаем в исходники и видим что тут у нас есть HIR, MIR и Types.

Что же происходит в реальности? Ну, для начала мы начинаем обработку AST. Этим, кстати занимается ещё один модуль, rust_ast_lowering . Смотрим туда и находим достаточно длинный файл, в котором и происходит преобразование каждого элемента AST в HIR.

Здесь весь синтаксический сахар растворяется в чае и перестаёт быть сахаром. Так моя любимая for node in data превращается в

А вот здесь, как раз, всеми любимый оператор ? первращается в Try::into_result :

С HIR теперь можно работать…

Шаг седьмой: rustc_ty

И .\rust-master\compiler\rustc_middle\src\ty\mod.rs . Одна из самых больших частей компилятора занимается проверками системы типов после того, как у нас есть HIR. Какой тип будет у let mut a = 5; ? Вот на этот вопрос и ответит наша система работы с типами. Две основных структуры здесь:

Последняя тянется через весь процесс компиляции.

Файл просто огромный. Нам надо вычислить типы каждой переменной, замыкания и трейта. Сам модуль занимает более 3000 строк, не считая остальные файлы в директории.

Кстати, смотрим в rust-master\compiler\rustc_typeck\src\check\expr.rs

Хмм.. Если мы натыкаемся на брейк, после которого есть только один лейбл - rust то нужно запустить функцию fatally_break_rust .

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

Пасхалки они выглядят именно вот так.

Так, вычислили типы и теперь можем проверить что никто не пытается запихнуть строку в Int. Хорошо. Можно идти дальше.

Шаг первый: rustc

Открываем сорцы и наслаждаемся. Всё выглядит очень прилично и чисто. Тут, понятное дело, можно учиться тому как правильно разделять свой проект на куски и как правильно управлять кодом на rust. Собственно говоря, сразу понятно куда идти. Забираемся в compiler/rustc/src/main.rs и смотрим.

Всё только начинается. Держитесь.

Всё только начинается. Держитесь.

Хм. То есть точка входа в программу просто тянет jemalloc вызовы и запускает ещё две функции. Ну вот, всё. Теперь понятно как работает компилятор rust. Делов-то! Кстати, jemalloc это специальный менеджер памяти, изначально разработанный для FreeBSD в 2005 году. Основной упор был сделан на то, чтобы избежать фрагментации памяти при работе с этим аллокатором. В оригинальной версии он просто заменяет malloc. В 2007 году Firefox начал использовать этот менеджер для снижения расхода памяти, а ещё через пару лет он попал в Facebook.

Словарь

Далее по тексту я буду разбрасываться следующими терминами без удержи. Если вы знаете что это всё значит - хорошо. Если нет, убедитесь что подтянули свои знания, перед тем как заныривать.

LLVM - система компиляции которая состоит из компилятора и набора инструментов, позволяющая создавать фронт-энд для любого языка и компилировать его на множество различных платформ.

AST - (abstract syntax tree) древовидная репрезентация семантической структуры исходного кода. Каждый узел обычно показывает конструкцию, встречающуюся в коде.

IR (intermediate representation) - Структура данных, обычно используемая в кишках компилятора или виртуальной машины, для представления исходного кода программы. Такую структуру обычно оптимизируют и перегоняют в конечный код.

HIR (High Level IR) - IR высокого уровня. Это основная репрезентация кода, используемая в rust. Фактически это представление AST, которым компилятору удобно пользоваться.

MIR (Mid Level IR) - Это репрезентация HIR, которая намного ближе к LLVMIR.

LLVMIR (Language Independent IR) - фактически это высокоуровневый ассемблер, который не привязан к определённому языку или системе. Такой код удобно оптимизировать и после он передаётся компилятору.

Крейт, crate - Это то, что будет скомпилировано либо в библиотеку или бинарник. На выходе будет одна библиотека или бинарник, вне зависимости от того, сколько файлов входят в крейт.

ICE (Internal compiler error), ошибка компилятора.

Дальнейший текст подразумевает, что вы умеете программировать. Можно и не на rust.

Читайте также: