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

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

重点

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)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Syncovery for Mac v10.14.3激活版:文件备份和同步工具

Syncovery for Mac是一款高效且灵活的文件备份与同步工具,专为Mac用户设计,旨在确保数据的安全性和完整性。该软件支持多种备份和同步方式,包括本地备份、网络备份以及云备份,用户可以根据实际需求选择最合适的方案。 Syncovery f…

全科都收!1区毕业水刊,影响因子狂涨至9.8,无预警记录!国人评价高!

本期,小编给大家解析的是一本创刊于2014年,且于同年被WOS数据库收录的毕业“水刊”——SCIENTIFIC DATA。 截图来源:期刊官网 SCIENTIFIC DATA(ISSN:2052-4463)是一本致力于数据的开放获取期刊&#xff0c…

linux将一个文件移动或复制到另一个目录下(超详细)

问题:需要在linux中将一个文件移动或复制到另一个目录下 下面提到的目录,可以直观理解为window中的文件夹 1、mv命令 mv是"move"的缩写,用于移动文件或目录到另一个位置。 将 文件 a.txt 移动到 目录home下 mv a.txt home将 目录…

基于SpringBoot的宠物领养网站管理系统

基于SpringBootVue的宠物领养网站管理系统的设计与实现~ 开发语言:Java数据库:MySQL技术:SpringBootMyBatis工具:IDEA/Ecilpse、Navicat、Maven 系统展示 主页 宠物领养 宠物救助站 宠物论坛 登录界面 管理员界面 摘要 基于Spr…

二叉搜索树中的搜索(力扣700)

解题思路:因为二叉搜索树的左小右大特点,中只是寻找比较目标,怎么序遍历都可以,终止条件就是遍历完毕和找到了,然后就遍历左右子树开始寻找就好了 具体代码如下: class Solution { public: TreeNode* searchBST(Tre…

基于springboot+vue+Mysql的房产销售平台

开发语言:Java框架:springcloudJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包&#xff1a…

Pycharm破解流程

1.下载pycharm 网上很多,随便找一个,懒得找的话,或者去我传上去的资源pycharm部分直接取 2.下载文件 文件部分,我放在pycharm文件里面一起 打开下载好的激活包 3.执行脚本 先执行unisntall-all-users.vbs,直接双击打开&#xff0c…

Rumble Club上线时间+配置要求+游戏价格+加速器推荐

Rumble Club上线时间配置要求游戏价格加速器推荐 Rumble Club是一款基于物理的玩家大乱斗游戏,该作拥有丰富饱满的视觉效果和趣味性十足的游玩极致,让玩家可以各种富有想象力的方式,推搡、戏耍好友。该作即将正式上线,为了避免玩…

DDP、pytorch的分布式 torch.distributed.launch 训练说明

0、DDP的运行原理 执行步骤: 将data分为多个不同的batch,每个gpu得到batch都是不一样的然后将每个batch放在每个gpu上独立的执行最后得到的梯度求平均将平均梯度平分给每个gpu执行下一次迭代 这也就意味着你有多少个gpu,训练的速度也会提升…

Centos7虚拟机与真机乎ping以及虚拟机ping不通的原因

以下是本机的网络地址 首先我在这两天的学习中遇到了以下种种问题: 本机与虚拟机互相ping不通虚拟机无法连接网络访问互联网本机可以ping通虚拟机 但是虚拟机不能ping本机 解决方法: 我在这里把dhcp改成了静态ip地址 这样以后不管怎么变化IP地址都没…

# 从浅入深 学习 SpringCloud 微服务架构(四)Ribbon

从浅入深 学习 SpringCloud 微服务架构(四)Ribbon 段子手168 一、ribbon 概述以及基于 ribbon 的远程调用。 1、ribbon 概述: Ribbon 是 Netflixfa 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。 在 SpringCloud 中 Eureka …

汽车摄像头匿名化处理解决方案,保护信息的安全性和隐私性

随着智能交通和自动驾驶技术的迅猛发展,汽车摄像头已成为现代汽车不可或缺的一部分,摄像头所捕捉的图像信息也引发了日益严峻的信息安全问题。如何在充分利用摄像头功能的同时,保障个人隐私和信息安全,已成为企业亟待解决的问题。…

芒果超媒的“乘风破浪”,差了一点市场海浪的反馈

4月21日晚间,芒果超媒发布了2023年度&2024一季度报告。 芒果超媒2023年实现营业收入146.28亿元,同比增长4.66%;净利润35.56亿元,同比增长90.73%;基本每股收益1.90元。公司拟每10股派发现金红利1.8元。2024年第一季…

创建虚拟环境(记录一下)

conda create -n name python3.8版本高于11.7: pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu117 --force-reinstall --user 检验是否为true import torch print(torch.cuda.is_available()) stable diff…

AI应用开发:pgvector能帮你解决什么问题

在这篇博客文章中,我们将探讨pgvector如何帮助PostgreSQL中的基于AI的工作负载,使您的数据库向量操作更快、更高效。 pgvector:在PostgreSQL中存储和查询向量 pgvector 是一个PostgreSQL扩展,允许您存储、查询和索引向量。 截至…

MTK6775/MT6775/曦力P70联发科处理器性能参数资料

联发科MT6775(曦力P70)芯片搭载强大的Arm Cortex-A73/A53八核CPU,并采用台积电12纳米FinFET制程工艺,相较于其他14纳米级别产品,功耗节省达到了15%。此外,曦力P70还配备了高效能的Arm Mali-G72 GPU,相比上一代产品曦力…

《QT实用小工具·三十六》metro风格的主界面

1、概述 源码放在文章末尾 该项目实现了metro风格的主界面,包含访客登记,记录查询,证件扫描,信息打印,系统设置,系统重启等功能,项目demo演示如下所示: 源码下载

JDK 11下载、安装、配置

下载 到Oracle管网下载JDK 11,下载前需要登录,否则直接点下载会出现502 bad gateway。 下载页面链接 https://www.oracle.com/hk/java/technologies/downloads/#java11-windows 登录 有些人可能没有Oracle账号,注册也比较慢,有需…

Springboot 初始化操作

文章目录 程序启动后的初始化ApplicationRunner 接口CommandLineRunner 接口ApplicationRunner 和CommandLineRunner 的区别 Bean的实例化InitializingBean 接口PostConstruct注解BeanInitializingBean 接口、PostConstruct注解、Bean注解区别 总结 前些天发现了一个巨牛的人工…

探索以太坊世界:使用Geth打造你的私人网络

文章目录 概要名词解释Geth(Go Ethereum)区块链网络种类 具体流程下载geth客户端配置私链新建创世区块启动私链 连接MetaMask钱包小结 概要 在区块链领域,以太坊私链的搭建是学习和开发智能合约的重要一步。私链允许开发者在独立的环境中进行…