При реализации потоков ядра в прошлом нам было достаточно указать начальную позицию для каждого потока, поскольку весь код находился в операционной системе. Однако теперь нам необходимо загрузить код и данные пользовательской программы из ELF-файла и отобразить их в памяти.
Конечно, нам не нужно реализовывать анализатор ELF-файлов самостоятельно, так как существует crate под названием xmas-elf
, который делает это за нас.
xmas-elf
Примечание: если IDE не может определить типы в коде, вы можете обратиться к документации (rustdoc
) для получения дополнительной информации о crate.
Сначала xmas-elf
считывает содержимое ELF-файла в память. На основе предыдущей главы о файловой системе мы легко можем добавить метод, который считывает весь файл как массив байтов ([u8]
) в структуру INode
:
fn readall(&self) -> Result<Vec<u8>> {
// Считываем размер из заголовка файла
let size = self.metadata()?.size;
// Создаём вектор и считываем данные
let mut buffer = Vec::with_capacity(size);
unsafe { buffer.set_len(size) };
self.read_at(0, buffer.as_mut_slice())?;
Ok(buffer)
}
В ELF-файле адреса различных полей обычно не являются непрерывными, и права доступа также могут различаться. Мы используем интерфейс библиотеки xmas-elf
, чтобы создать набор отображений памяти на основе данных, считанных из ELF-файла.
Обратите внимание, что пользовательская программа также сначала отображает все пространство ядра, иначе она не сможет обрабатывать прерывания.
/// Создание набора отображений памяти из ELF-файла (без стека)
pub fn from_elf(file: &ElfFile, is_user: bool) -> MemoryResult<MemorySet> {
// Создание набора отображений с ядром
let mut memory_set = MemorySet::new_kernel()?;
// Перебираем все части ELF-файла
for program_header in file.program_iter() {
if program_header.get_type() != Ok(Type::Load) {
continue;
}
// Читаем начальный адрес, размер и данные из каждого поля
let start = VirtualAddress(program_header.virtual_addr() as usize);
let size = program_header.mem_size() as usize;
let data: &[u8] =
if let SegmentData::Undefined(data) = program_header.get_data(file).unwrap() {
data
} else {
return Err("unsupported elf format");
};
// Преобразуем каждую часть в сегмент
let segment = Segment {
map_type: MapType::Framed,
range: Range::from(start..(start + size)),
flags: Flags::user(is_user)
| Flags::readable(program_header.flags().is_read())
| Flags::writable(program_header.flags().is_write())
| Flags::executable(program_header.flags().is_execute()),
};
// Добавляем отображение и копируем данные
memory_set.add_segment(segment, Some(data))?;
}
Ok(memory_set)
}
Мы размышляем: когда мы создаём отображение для пользовательской программы, виртуальный адрес указан в ELF-файле, а физический адрес — это адрес хранения данных на диске? Есть ли в этом какие-то проблемы?
Мы можем не заметить этого при работе на симуляторе, но прямое сопоставление дискового пространства приведёт к огромным задержкам при использовании. Поэтому операционная система обычно копирует только небольшое количество данных при подготовке программы к запуску, а остальные загружаются по мере необходимости. Конечно, наша простая операционная система загружает всё сразу в память.
Кроме того, даже если мы хотим напрямую сопоставить дисковое пространство, это не всегда возможно. Это связано с тем, что смещение внутри страницы остаётся неизменным при преобразовании виртуального адреса в физический. Таким образом, невозможно гарантировать, что адреса, указанные в ELF, соответствуют адресам на диске.
Мы модифицируем функцию Mapping::map
, добавляя параметр для инициализации данных. При реализации необходимо учитывать несколько важных деталей:
memcpy
?В текущей структуре только при запуске потока обновляется таблица страниц. Поэтому, если мы не обновим таблицу страниц и не обновим TLB после отображения каждой страницы, виртуальные адреса будут недоступны.
Однако мы получаем физические адреса страниц через распределитель, которые уже находятся в линейном отображении ядра. Поэтому здесь фактически используются физические адреса для записи данных.
Конкретная реализация может быть найдена в функции Mapping::map
в файле os/src/memory/mapping/mapping.rs
.
Теперь мы можем запускать пользовательские программы в нашей операционной системе, используя данные из образа диска. Пример кода выглядит следующим образом:
// Находим программу в файловой системе
let app = fs::ROOT_INODE.find("hello_world").unwrap();
// Считываем данные
let data = app.readall().unwrap();
// Анализируем ELF-файл
let elf = ElfFile::new(data.as_slice()).unwrap();
// Создаём поток из ELF-файла, отображаем пространство и загружаем данные
let process = Process::from_elf(&elf, true).unwrap();
// Получаем адрес входа программы из ELF
let thread = Thread::new(process, elf.header.pt2.entry_point() as usize, None).unwrap();
// Добавляем поток
PROCESSOR.lock().add_thread(thread);
К сожалению, мы не можем использовать print
непосредственно в пользовательской программе, как в потоках ядра. Первые основаны на машинно-независимых вызовах OpenSBI, в то время как для печати символов в пользовательском процессе нам потребуется реализовать системные вызовы в операционной системе, чтобы предоставить услуги пользовательским процессам.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )