1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/rcore-os-rCore-Tutorial

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
part-3.md 9.9 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 29.11.2024 21:14 7f8a4fd

Анализ ELF-файла и создание потока

При реализации потоков ядра в прошлом нам было достаточно указать начальную позицию для каждого потока, поскольку весь код находился в операционной системе. Однако теперь нам необходимо загрузить код и данные пользовательской программы из 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, добавляя параметр для инициализации данных. При реализации необходимо учитывать несколько важных деталей:

  • Поскольку распределение памяти для пользовательских программ является динамическим, отдельные страницы могут не быть смежными, поэтому необходимо рассматривать каждую страницу отдельно.
  • Длина каждого поля не обязательно кратна размеру страницы, поэтому следует учитывать ситуацию, когда требуется копирование неполной страницы.
  • Программа имеет раздел bss, который не сохраняется в ELF, и его необходимо обнулить при загрузке в память.
  • Для каждой страницы есть физический адрес, виртуальный адрес и адрес данных для загрузки. Можно ли просто скопировать данные с адреса данных для загрузки на виртуальный адрес, как это делается с помощью memcpy?

В текущей структуре только при запуске потока обновляется таблица страниц. Поэтому, если мы не обновим таблицу страниц и не обновим TLB после отображения каждой страницы, виртуальные адреса будут недоступны.

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

Конкретная реализация может быть найдена в функции Mapping::map в файле os/src/memory/mapping/mapping.rs.

Запуск Hello World?

Теперь мы можем запускать пользовательские программы в нашей операционной системе, используя данные из образа диска. Пример кода выглядит следующим образом:

// Находим программу в файловой системе
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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/rcore-os-rCore-Tutorial.git
git@api.gitlife.ru:oschina-mirror/rcore-os-rCore-Tutorial.git
oschina-mirror
rcore-os-rCore-Tutorial
rcore-os-rCore-Tutorial
master