Я пытаюсь написать загрузчик 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 загрузчика и полную дизассемблирование, но я могу добавить его, если кто-то считает, что это может помочь.
Да, это вызывает беспокойство. Вызовы функций могут включать в себя пролог, который может включать любую инструкцию, включая те, которые манипулируют стеком.
К счастью, в 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 для остановки ЦП в случае возврата основного процессора.
main()
должна быть функцией extern "C"
, поскольку соглашение о вызовах для Rust не указано.
Кроме того, шаг остановки принято записывать как
1: cli ; hlt ; jmp 1b
или хотя быcli ; hlt
, чтобы защитить от включения прерываний при его достижении.