Как подготовить указатель стека для ржавчины на голом металле?

Я пытаюсь написать загрузчик x86 и операционную систему полностью на Rust (без отдельных файлов сборки, только встроенная сборка внутри Rust).

Мой загрузчик в эмуляторе QEMU работает полностью так, как задумано, я углубился в разработку своего ядра, и мой загрузчик ни разу не подвел. Меня просто беспокоит, не имеет ли мой загрузчик какое-либо неопределенное поведение, и это поведение мне подходит.

Одна из первых вещей, которые мне нужно сделать для «загрузчика», — это установить указатель стека на действительную область памяти, которая будет действовать как стек моего загрузчика. Мой загрузчик использует локальные переменные, а также устанавливает указатель стека с помощью встроенной сборки.

// extreme oversimplification of what I have as my bootloader

#![no_std]
#![no_main]

#[no_mangle]
fn entry() -> !
{
    // place the stack area just before the boot sector
    unsafe { core::arch::asm!
    (
        "mov sp, 0x7c00"
    )}

    let bootloader_variable_1 = ...;
    let bootloader_variable_2 = ...;
    // do things with bootloader variables
}

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

Глядя на дизассемблирование (синтаксис x86 Intex) сгенерированного двоичного файла при сборке в режиме выпуска, я вижу...

push bx
sub sp, 12

mov sp, 0x7c00
...

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

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

Является ли это поводом для беспокойства? Могу ли я безопасно установить указатель стека в среде no_std (в самом начале программы) и не повредить локальные переменные? Будет ли эта схема работать на всех без исключения компиляторах, совместимых с Rust? Если нет, есть ли способ сделать это без какого-либо внешнего файла сборки?

Я исключил свой полный код этапа 1 загрузчика и полную дизассемблирование, но я могу добавить его, если кто-то считает, что это может помочь.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
22
0
1 545
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

К счастью, в Rust есть решение. К сожалению, оно пока нестабильно. К счастью, было решено стабилизировать ситуацию. К сожалению, этого до сих пор не было (с 2022 года). К счастью, есть крейт, который покрывает 99% случаев использования, включая ваш (на самом деле это всего лишь оболочка global_asm!()).

Решение — голые функции, функции, которые гарантированно не имеют пролога или эпилога. К сожалению, это означает, что компилятор не может полагаться на свойства, необходимые для выполнения кода Rust, а это значит, что все, что у него может быть, — это большой гигантский блок asm!().

Но это не значит, что вам нужно писать код на ассемблере. Вы можете создать функцию extern "C", содержащую ваш код. Поскольку это extern "C", у него будет известное соглашение о вызовах, что означает, что вы можете вызвать его из своей сборки после выполнения необходимой настройки. Но поскольку компилятору Rust разрешено вставлять пролог/эпилог, вы можете писать туда обычный Rust-код. Но он будет работать только после вашей настройки, если это необходимо.

Ответ принят как подходящий

Способ сделать это — создать _entry голую функцию, для которой компилятор не будет генерировать какой-либо дополнительный ассемблерный код, кроме того, что вы указали.

Голые функции в настоящее время являются экспериментальной функцией и потребуют ночного компилятора и функции #![feature(naked_functions)]. Или вы можете использовать крейт nude_function, который реализует голые функции с помощью макроса global_asm!, доступного начиная с Rust 1.59.0.

В любом случае вы затем напишете свою функцию входа следующим образом:

#[naked]
pub extern "C" fn entry() -> ! {
    unsafe {
        asm!(
            "mov sp, 0x7c00",
            "call {main}",
            "hlt",
            main = sym main,
            options(noreturn)
        );
    }
}

Это устанавливает указатель стека, а затем вызывает функцию main в качестве реальной точки входа; Затем main можно записать как обычную функцию Rust, как только стек настроен. Затем он выдает HLT для остановки ЦП в случае возврата основного процессора.

Кроме того, шаг остановки принято записывать как 1: cli ; hlt ; jmp 1b или хотя бы cli ; hlt, чтобы защитить от включения прерываний при его достижении.

user3840170 06.07.2024 17:30
main() должна быть функцией extern "C", поскольку соглашение о вызовах для Rust не указано.
Chayim Friedman 07.07.2024 04:20

Другие вопросы по теме