rCore-Turorial-Book第三课(计算机启动流程和程序内存布局与编译流程探索)

news2024/11/16 5:00:54

本节任务:梳理程序在操作系统中被编译运行的全流程,大体了解我们在没有操作系统的情况下,我们会面对那些困难

重点

1. 计算机组成基础

面对的困难:没有操作系统,我们必须直面硬件资源,管理起他们并为应用程序提供高效的抽象。

  • 计算机主要由 处理器CPU),物理内存和**I/O外设**三部分组成

  • CPU 唯一能够直接访问的只有物理内存中的数据

  • CPU 通过访存指令访问物理内存中的数据

  • CPU多个字节为单位访问物理内存

Tips:CPU 的视角看来,可以将物理内存看成一个大字节数组,而物理地址则对应于一个能够用来访问数组中某个元素的下标。与我们日常编程习惯不同的是,该下标通常不以 0 开头,而通常以一个常数,如 0x80000000 开头。简言之,CPU 可以通过物理地址来寻址,并 逐字节 地访问物理内存中保存的数据。

问题:

因为 CPU 以多个字节为单位访问物理内存,会引发两个值得考虑的问题。

  • 字节读取顺序 (大小端序问题)
  • 内存地址对齐

2. QEMU模拟器

使用QEMU模拟器说明

  • 为了方便实验,在此用 QEMU 模拟器模拟出一个裸机环境,我们须实现将内核运作在 QEMU中并检验其正确性。

  • 本实验使用 qemu-system-riscv64 模拟一台64位 Risc-v (精简指令集)架构的计算机

2.1 启动 QEMU

指令如下

$ qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios ../bootloader/rustsbi-qemu.bin \
    -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000

为了方便调用,我们可以将其写入 Makefile

# Building
TARGET := riscv64gc-unknown-none-elf
MODE := release
KERNEL_ELF := target/$(TARGET)/$(MODE)/os
KERNEL_BIN := $(KERNEL_ELF).bin
DISASM_TMP := target/$(TARGET)/$(MODE)/asm

# BOARD
BOARD := qemu
SBI ?= rustsbi
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin

# KERNEL ENTRY
KERNEL_ENTRY_PA := 0x80200000

run: run-inner

run-inner: build
        @qemu-system-riscv64 $(QEMU_ARGS)

QEMU_ARGS := -machine virt \
                         -nographic \
                         -bios $(BOOTLOADER) \
                         -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)

  • -machine virt 表示将模拟的 64 位 RISC-V 计算机设置为名为 virt 的虚拟计算机。我们知道,即使同属同一种指令集架构,也会有很多种不同的计算机配置,比如 CPU 的生产厂商和型号不同,支持的 I/O 外设种类也不同。Qemu 还支持模拟其他 RISC-V 计算机
  • -nographic 表示模拟器不需要提供图形界面,而只需要对外输出字符流。
  • 通过 -bios 可以设置 Qemu 模拟器开机时用来初始化的引导加载程序(bootloader),这里我们使用预编译好的 rustsbi-qemu.bin ,它需要被放在与 os 同级的 bootloader 目录下,该目录可以从每一章的代码分支中获得。
  • 通过虚拟设备 -device 中的 loader 属性可以在 Qemu 模拟器开机之前将一个宿主机上的文件载入到 Qemu 的物理内存的指定位置中, fileaddr 属性分别可以设置待载入文件的路径以及将文件载入到的 Qemu 物理内存上的物理地址。这里我们载入的 os.bin 被称为 内核镜像 ,它会被载入到 Qemu 模拟器内存的 0x80200000 地址处。 那么内核镜像 os.bin 是怎么来的呢?上一节中我们移除标准库依赖后会得到一个内核可执行文件 os ,将其进一步处理就能得到 os.bin ,具体处理流程我们会在后面深入讨论。

2.2 QEMU 启动流程

Tips:

  • Qemu模拟的 virt 硬件平台上,物理内存的起始物理地址为 0x80000000 ,物理内存的默认大小为 128MiB
  • 上述指令启动 QEMU,在 QEMU 执行任何指令之前,会把两个文件加载到 QEMU 的物理内存中
    • 作为 bootloader rustsbi-qemu.bin 加载到物理内存以物理地址 0x80000000 开头的区域上
    • 把内核镜像 os.bin 加载到以物理地址 0x80200000 开头的区域上

QEMU 加电启动有三个阶段,每一个阶段都有一层软件或者是固件负责,承担起计算机的初始化工作。

每当当前阶段执行完毕,就会跳转到下一层软件或固件的入口地址,将计算机的控制权转移给了下一层软件或固件。

2.2.1 启动第一阶段:固化在QEMU内的汇编程序
  • 将必要的文件载入到 Qemu 物理内存之后,Qemu CPU 的程序计数器(PC, Program Counter)会被初始化为 0x1000
  • Qemu 实际执行的第一条指令位于物理地址 0x1000
  • 执行寥寥数条指令并跳转到物理地址 0x80000000 对应的指令处并进入第二阶段。
2.2.2 启动第二阶段:bootloader
  • 由于 Qemu 的第一阶段固定跳转到 0x80000000 ,我们需要将负责第二阶段的 bootloader rustsbi-qemu.bin 放在以物理地址 0x80000000 开头的物理内存中
  • bootloader 负责对计算机进行一些初始化工作,并跳转到下一阶段软件的入口,在 Qemu 上即可实现将计算机控制权移交给我们的内核镜像 os.bin
  • 对于不同的 bootloader 而言,下一阶段软件的入口不一定相同
  • 获取进入下一阶段信息的方式和时间点也有所不同
    • 入口地址可能是一个预先约定好的固定的值
    • 也有可能是在 bootloader 运行期间才动态获取到的值。
  • 我们选用的 RustSBI 则是将下一阶段的入口地址预先约定为固定的 0x80200000
2.2.3 启动第三阶段:内核镜像
  • 为了正确地和上一阶段的 RustSBI 对接,我们需要保证内核的第一条指令位于物理地址 0x80200000
  • 将内核镜像预先加载到 Qemu 物理内存以地址 0x80200000 开头的区域上
  • 一旦 CPU 开始执行内核的第一条指令,证明计算机的控制权已经被移交给我们的内核

2.3 总结

为了让内核镜像能够正确对接到 QEMURustSBI上,我们的内核镜像文件必须满足

  • 该文件的开头即为内核待执行的第一条指令
  • 如果不满足上述条件,我们还需要对可执行文件进行一些操作才能得到可提交给 Qemu 的内核镜像

3. 程序内存布局与编译流程

3.1 程序内存布局

当我们将源代码编译成为可执行文件后,我们就看不大懂了,但是我们知道,至少这些字节可以被分为代码和数据两个部分

  • 代码部分:由一条条可以被 CPU 解码并执行的指令组成

  • 数据部分:被 CPU 视作可读写的内存空间

但是我们还可以进一步把上述部分分为更小的单位:不同的段会被编译器放置在内存的不同位置

X86时有四个段寄存器分别指向不同的段

  • cs: 代码段
  • ds: 数据段
  • ss: 栈段
  • es:扩展段

img

​ 有了这个概念后,就构成了程序的经典内存布局Memory Layout

MemoryLayout

按照这个经典内存结构,可以看到代码部分只有

  • 代码段.text一个,存放所有汇编代码
  • 数据段
    • .rodata 存放只读全局数据,如常量,常量字符串
    • .data 存放可修改全局数据
    • .bss 保存程序中未初始化的全局数据,通常由程序的加载者将这些部分进行零初始化,就是将这块区域逐个字节清零
    • heap 存放程序运行时动态分配的数据,它向高地址增长
    • stack 不仅用作函数调用上下文的保存与恢复每个函数作用域内的局部变量也被编译器放在它的栈帧内向低地址增长

3.2 编译流程

从源码得到可执行文件的编译流程有多个阶段

  1. 编译器 (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII其他编码 的文本文件;
  2. 汇编器 (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的 目标文件 (Object File);
  3. 链接器 (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。

显然汇编器会对每一个目标文件都生成一个独立的程序内存布局,它描述的是目标文件内各段所在的位置

但是问题来了,一个工程,可能有成千上万份文件,而且这些文件之间互相关联

链接器的作用就是将所有的目标文件整合成为一个整体内存布局

3.2.1 链接器第一项工作

将来自不同目标文件的段在目标内存布局中重新排布

在链接过程中,我们把每个目标文件的各个段按照段功能进行分类,把功能相同的段被排载一起放在拼接后的新目标文件中

注意到,目标文件 1.o2.o内存布局是存在冲突的同一个地址在不同的内存布局中存放不同的内容。而在合并后的内存布局中,这些冲突被消除。

link-sections

3.2.1 链接器第二项工作

将符号替换为具体地址

符号何时被替换为具体地址呢?

​ 因为符号对应的变量或函数都是放在某个段里面的固定位置(如全局变量往往放在 .bss 或者 .data 段中,而函数则放在 .text 段中),所以我们需要等待符号所在的段确定了它们在内存布局中的位置之后才能知道它们确切的地址。当一个模块被转化为目标文件之后,它的内部符号就已经在目标文件中被转化为具体的地址了,因为目标文件给出了模块的内存布局,也就意味着模块内的各个段的位置已经被确定了。

​ 然而,此时模块所用到的外部符号的地址无法确定。我们需要将这些外部符号记录下来,放在目标文件一个名为符号表(Symbol table)的区域内。由于后续可能还需要重定位,内部符号也同样需要被记录在符号表中。

重定位

​ 外部符号需要等到链接的时候才能被转化为具体地址。假设文件 1 用到了文件 2 所提供的内容,当两个模块的目标文件链接到一起的时候,它们的内存布局会被合并,也就意味着两个模块的各个段的位置均被确定下来。此时,文件 1 用到的来自文件 2 的外部符号可以被转化为具体地址

​ 注意:两个模块的段在合并后的内存布局中被重新排布其最终的位置有可能和它们在模块自身的局部内存布局中的位置相比已经发生了变化。因此,每个模块的内部符号的地址也有可能会发生变化,我们也需要进行修正。

Tips:这里的符号指什么呢?

  • 在我们进行模块化编程时,每个模块都会提供一些向其他模块公开的全局变量函数等供其他模块访问,也会访问其他模块向它公开的内容
  • 访问一个变量或者调用一个函数,在源代码级别我们只需知道它们的名字即可,这些名字被我们称为符号
  • 我们还可以根据其来源于模块内部还是其他模块,可将符号分为内部符号外部符号
  • 机器码级别(也即在目标文件或可执行文件中)我们并不是通过符号来找到索引我们想要访问的变量或函数,而是直接通过变量或函数的地址
  • 调用一个函数,那么在指令的机器码我们可以找到函数入口的绝对地址或者相对于当前 PC 的相对地址

4. 补充解释

局部变量和全局变量

在一个函数的视角中,它能够访问的变量包括以下几种:

  • 函数的输入参数局部变量:保存在一些寄存器或是该函数的栈帧里面,如果是在栈帧里面的话是基于当前栈指针加上一个偏移量来访问的
  • 全局变量:保存在数据段 .data.bss 中,某些情况下 gp(x3) 寄存器保存两个数据段中间的一个位置,于是全局变量是基于 gp 加上一个偏移量来访问的
  • 堆上的动态变量本体被保存在堆上,大小在运行时才能确定。而我们只能 直接 访问栈上或者全局数据段中的 编译期确定大小 的变量。因此我们需要通过一个运行时分配内存得到的一个指向堆上数据的指针来访问它,指针的位宽确实在编译期就能够确定。该指针即可以作为局部变量放在栈帧里面,也可以作为全局变量放在全局数据段中。

真实计算机的加电启动流程

  • 第一阶段:加电后 CPU PC 寄存器被设置为计算机内部只读存储器(ROM,Read-only Memory)的物理地址,随后 CPU 开始运行 ROM 内的软件。我们一般将该软件称为固件(Firmware),它的功能是对 CPU 进行一些初始化操作,将后续阶段的 bootloader 的代码、数据从硬盘载入到物理内存,最后跳转到适当的地址将计算机控制权转移给 bootloader 。它大致对应于 Qemu 启动的第一阶段,即在物理地址 0x1000 处放置的若干条指令。可以看到 Qemu 上的固件非常简单,因为它并不需要负责将 bootloader 从硬盘加载到物理内存中,这个任务此前已经由 Qemu 自身完成了。

  • 第二阶段:bootloader 同样完成一些 CPU 的初始化工作,将操作系统镜像从硬盘加载到物理内存中,最后跳转到适当地址将控制权转移给操作系统。可以看到一般情况下 bootloader 需要完成一些数据加载工作,这也就是它名字中 loader 的来源。它对应于 Qemu 启动的第二阶段。在 Qemu 中,我们使用的 RustSBI 功能较弱,它并没有能力完成加载的工作,内核镜像实际上是和 bootloader 一起在 Qemu 启动之前加载到物理内存中的。

  • 第三阶段:控制权被转移给操作系统。由于篇幅所限后面我们就不再赘述了。

值得一提的是,为了让计算机的启动更加灵活,bootloader 目前可能非常复杂:它可能也分为多个阶段,并且能管理一些硬件资源,从复杂性上它已接近一个传统意义上的操作系统。

端序或尾序

​ 端序或尾序(Endianness),又称字节顺序。在计算机科学领域中,指电脑内存中或在数字通信链路中,多字节组成的字(Word)的字节(Byte)的排列顺序。字节的排列方式有两个通用规则。例如,将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序(little-endian);反之则称大端序(big-endian)。常见的 x86RISC-V 等架构采用的是小端序。

内存地址对齐

​ 内存地址对齐是内存中的数据排列,以及 CPU 访问内存数据的方式,包含了基本数据对齐和结构体数据对齐的两部分。CPU 在内存中读写数据是按字节块进行操作,理论上任意类型的变量访问可以从内存的任何地址开始,但在计算机系统中,CPU 访问内存是通过数据总线(决定了每次读取的数据位数)和地址总线(决定了寻址范围)来进行的,基于计算机的物理组成和性能需求,CPU 一般会要求访问内存数据的首地址的值为 4 或 8 的整数倍。

基本类型数据对齐是指数据在内存中的偏移地址必须为一个字的整数倍,这种存储数据的方式,可以提升系统在读取数据时的性能。结构体数据对齐,是指在结构体中的上一个数据域结束和下一个数据域开始的地方填充一些无用的字节,以保证每个数据域(假定是基本类型数据)都能够对齐(即按基本类型数据对齐)。

​ 对于RISC-V处理器而言,load/store 指令进行数据访存时,数据在内存中的地址应该对齐。如果访存 32 位数据,内存地址应当按 32 位(4字节)对齐。如果数据的地址没有对齐,执行访存操作将产生异常。这也是在学习内核编程中经常碰到的一种 bug。

5. 参考文档

内核第一条指令(基础篇) - rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1615376.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

本地环境通过ssh通道连接服务器数据库,实现本地客户端和代码可以访问数据库

使用方法: ssh -p 搭建隧道的端口 -fNL 本地端口:远程ip:远程端口号 搭建隧道的账号搭建隧道的ip 可以增加参数-v,输出更多的信息 ssh -p 搭建隧道的端口 -fNL 本地端口:远程ip:远程端口号 -v 搭建隧道的账号搭建隧道的ip 有时候,测试环境的数据库不允许…

YOLOv8-PySide --- 基于 ultralytics 8.1.0 发行版优化 | 代码已开源

YOLOv8-PySide — 基于 ultralytics 8.1.0 发行版优化 Github 项目地址:https://github.com/WangQvQ/Ultralytics-PySide6 BiliBili视频地址:https://www.bilibili.com/video 页面效果 如何使用 pip install ultralytics8.1.0 or git clone --branch v…

如何判别三角形和求10 个整数中最大值?

分享每日小题,不断进步,今天的你也要加油哦!接下来请看题------> 一、已知三条边a,b,c能否构成三角形,如果能构成三角形,判断三角形的类型(等边三角形、等腰三角形或普通三角形 …

通过Docker新建并使用MySQL数据库

1. 安装Docker 确保您的系统上已经安装了Docker。可以通过以下命令检查Docker是否安装并运行: systemctl status docker如果没有安装或运行,请按照官方文档进行安装和启动。 2. 拉取MySQL镜像 从Docker Hub拉取MySQL官方镜像。这里以MySQL 5.7版本为…

(回溯)记忆化搜索和dp

动态规划的核心就是 状态的定义和状态的转移 灵神 的 回溯改递归思路 首先很多动态规划问题都可以采用 回溯 的思想 回溯主要思想就是把 一个大问题分解成小问题 比如 采用子集类回溯问题中的核心思想-> 选或不选 或者 选哪个 记忆化搜索之后 我们可以发现 每个新节点依…

【网络原理】UDP协议的报文结构 及 校验和字段的错误检测机制(CRC算法、MD5算法)

目录 UDP协议 UDP协议的报文结构及注意事项 UDP报文结构中的校验和字段 1. 校验和主要校验的内容 2. UDP校验和的实现方式 3. CRC(循环冗余校验)算法 4. MD5(Message Digest Algorithm 5) UDP协议 上一篇文章提过&#xf…

【C++】双指针算法:复写零

1.题目 别看这是一道简单题,它的通过率低于一些中等甚至困难的题目! 大大增加这道题目难度的是最后一句话:1.不可越界写入。2.就地修改。 如果可以再创建一个数组的话,那么这道题目就会非常简单,但这道题目必须要求在…

汽车IVI中控开发入门及进阶(十六):carplay认证

现在有些中控采用高通的芯片如8155、8295等,实现多屏互动等,但是也有一些车型走低成本方案,比如能够实现HiCar、CarLife或者苹果Apple的Carplay等能进行手机投屏就好了。 能实现CarPlay功能通过Carplay认证,也就成了一些必须的过程,国产车规级中控芯片里,开阳有一款ARK1…

AI视频分析技术的常用开源模型及TSINGSEE青犀AI视频识别分析能力介绍

AI视频分析技术是指利用人工智能技术来对视频数据进行分析和处理的技术。开源模型是指可以免费获取和使用的代码模型,可以帮助开发人员快速构建和部署AI视频分析应用程序。 以下是一些业内常用的用于AI视频分析技术的开源模型: OpenCV:Open…

kali搭建vulfocus靶场

电脑安装kali,使用kali搭建靶场,自己电脑作为VPS使用 kali 先装好docker: # docker version # 查看当前是否有docker,如果无docker ,则进行docker安装 # apt install docker.io 安装完成后,再次输入 # docker…

【数据结构】二叉树链式结构的实现《遍历,实现》(题库+解析+源码)

前言 二叉树的学习离不开对堆的理解,这是上篇堆的传送门 http://t.csdnimg.cn/F6Jp3 1.二叉树链式结构的实现 1.1 前置说明 在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还…

【Java】常见锁策略 CAS机制 锁优化策略

前言 在本文会详细介绍各种锁策略、CAS机制以及锁优化策略 不仅仅局限于Java,任何和锁相关的话题,都可能会涉及到下面的内容。 这些特性主要是给锁的实现者来参考的. 普通的程序猿也需要了解一些, 对于合理的使用锁也是有很大帮助的 文章目录 前言✍一、…

收集统计信息报错ora-00600[16515]问题处理

1、基础环境 操作系统Oracle Solaris 11.3 数据版本Oracle 12.2 2、故障理像 对一个20T的大库收集一下全库的统计信息 原因是现在都2024年了,这个库的统计信息基本都是2021年, 没具体查找啥原因导致的系统自定义的收集任务失败,于是决定手…

2024年内外贸一体化融合发展(长沙)交易会

2024年内外贸一体化融合发展(长沙)交易会 一、总体思路 充分发挥湖南作为全国内外贸一体化试点地区作用,坚持“政府主导、市场驱动、企业为主”的原则,以“助力双循环,拓展新市场,促进新消费”为主题&…

腾讯云服务器价格明细表2024年最新(CPU内存/带宽/磁盘)

腾讯云服务器价格明细表2024年最新(CPU内存/带宽/磁盘)腾讯云服务器租用优惠价格表:轻量应用服务器2核2G3M价格61元一年,2核2G4M价格99元一年、135元15个月、540元三年,2核4G5M带宽165元一年、252元15个月、756元3年&a…

VBA运行后,为什么excel的三个工作表结果一样?

运行完了excel的三个工作表的结果一样,问题在哪呢? 代码如下: Sub 计算成绩() 计算成绩 Macro i为工作表行号 Dim i, m, total As Integer Dim w1 As Worksheet For m 1 To Worksheets.count Set w1 Worksheets(m) i 2 total 0 …

【MySQL 数据宝典】【磁盘结构】- 002 数据字典

一、数据字典 ( Data Dictionary ) 1.1 背景介绍 我们平时使用 INSERT 语句向表中插入的那些记录称之为用户数据,MySQL只是作为一个软件来为我们来保管这 些数据,提供方便的增删改查接口而已。但是每当我们向一个表中插入一条记录的时候,MyS…

ZISUOJ 数据结构--队列及其应用

说明: 基本都是bfs的常见模板题型,思路都很直接,不过后面有两道题很搞心态,它们给的坐标x、y是反的,导致刚开始一直错。题目还是要看仔细,不能先入为主。 题目列表: 问题 A: 围圈报数(完善程序…

Python实现对波士顿房价的分析与预测

文章目录 问题分析所需环境代码实现1. 相关性分析及可视化2. 房价分析及可视化3. 构建房价预测模型问题分析 波士顿房价数据集包含美国人口普查局收集的美国马萨诸塞州波士顿住房价格的有关信息, 数据集很小,只有506个案例。 数据集都有以下14个属性,具体含义如下 现在需要…

工业控制(ICS)---组态软件分析

组态软件 什么是组态软件? 组态软件就是一些数据采集与过程控制的专用软件,它们是在自动控制系统监控层一级的软件平台和开发环境,使用灵活的组态方式,为用户提供快速构建工业自动控制系统监控功能的通用层次的软件工具。 组态软…