《操作系统真象还原》第一章 配置实验环境,历经千辛万险

130 阅读29分钟

本实验参考笔记 love6.blog.csdn.net/article/det…

因为我想配置云端实验环境,尝试了azure VM创建ubuntu 虚拟机,但是当时我不懂配置gnome图形界面,所以在启动bochs 的时候看不到GUI,差点一度放弃;然后尝试azure vm winserver,在winserver内用hyper-v创建ubuntu 但是没有网络,配置了几天没搞懂怎么设置;最后尝试vncserver 获取ubuntuserver的图形化页面信息,才得以进行下去。期间曾经考虑是否要换到THU的rcore项目。

原理

花较短篇幅解释为什么要有这个实验环境

bochs安装和配置

基本参考 进行安装

1 下载tar.gz 

curl -L -O https://github.com/bochs-emu/Bochs/archive/refs/tags/REL_2_8_FINAL.tar.gz

因为教程依赖的是2.6.2 这个tag 所以先git clone 原始仓库,然后再进行checkout 到对应的tag 

注意要cd 到 bochs 文件夹执行下面命令

./configure \

--prefix=/your_path/bochs \

--enable-debugger \

--enable-disasm \

--enable-iodebug \

--enable-x86-debugger \

--with-x \

--with-x11

然后就是 sudo make && sudo make install

但是,如果你的ubuntu 是新的,会报错如下,有其他工具需要安装,这些问题遇到的时候问gpt

执行下面的

sudo apt update

sudo apt install libx11-dev libxext-dev libxmu-dev libxrandr-dev -y

还是报错

ERROR: pkg-config was not found, or unable to access the gtk+-2.0 package.

Install pkg-config and the gtk+ development package,

or disable the gui debugger, or the wxWidgets display library (whichever is being used).

执行下面

sudo apt update

sudo apt install pkg-config libgtk2.0-dev -y

再次运行 sudo make && sudo make install 成功,之后都要切换到一个名为cooil...的账户下实验

接下来还有bochs 配置文件需要修改

找到安装路径下的bochs 配置文件 /home/cooiboi/bochs

image.png

编辑这个配置文件 保存到 bochs 的启动文件bochs bin  所在的bin/ 文件夹下

image.png

需要创建硬盘否则启动bochs 失败, 使用bin/bximage 创建 执行 sudo ./bximage 他会在当前目录下创 hd ,

dd if=/home/lenny/git/assemble/mbr.bin  of=/home/lenny/git/assemble/bochs-2.6.8/hd60M.img  bs=512  count=1  conv=notrunc

结果输出如下图 image.png

我把bochsrc.disk 和bochs 存储在同一个文件夹,因此在启动bochs 的时候指定boch config file

问题1

可以看到,问题主要是CPU的 malform,  查询了bochsrc.disk 

以下部分可能是问题的根源:

cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"

问题分析

  1. model=core2_penryn_t9600 不被支持: Bochs 的 model 参数仅支持有限的 CPU 类型名称,比如 core2 或 pentium 等,而 core2_penryn_t9600 是无效的值。
  2. 配置项符合性问题: Bochs 的配置文件要求严格,任何额外的未定义参数都会导致解析错误。

解决方案

  1. 修正 model 值:替换为支持的 CPU 模型(例如 core2 或 pentium)。修改如下:

    cpu: model=core2, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
    
  2. 验证是否需要 msrs 参数:如果你不确定 msrs="msrs.def" 是否必要,可以暂时去掉此参数,简化配置,测试是否正常运行:

    cpu: model=core2, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1
    
  3. 确认 ips 参数值: ips 是指每秒执行的指令数。如果出现性能或兼容性问题,可以尝试减少到较小的值,例如:

    ips=1000000
    
  4. 测试配置:保存修改后的配置文件,重新运行 Bochs:成功

    ./bochs -f ./bochsrc.disk
    

问题2 

似乎是keyboard mapping  的配置无法理解 dam 大佬骗我 修改一下keyboard 就好

image.png

问题3  

需要关闭声卡

image.png

禁止使用之后的,再次执行终于没有额外问题 直接到6 

但是点击6 开始模拟 出现问题 如下

bochsrc.disk:85: display library 'nogui' not available

切换使用vnc viewer 终于正常显示

但是我的simulator 是一片黑,什么都没有,在大佬的笔记中看到这句话,确实我复现了,

image.png

在terminal  输入c ,simulator 有显示,如下图  

根据这个图,报错消息和大佬一样,no bootable device, 原因就是我们输入的CHS信息,这个硬盘空 间内没有合适的MBR程序

所以我们要把MBR binary file 拷贝到硬盘空间内

image.png

编写好mbr.S 并使用sudo nasm 编译成bin文件

使用这个命令考吧到硬盘:

dd if=./mbr.bin of=./hd60M.img bs=512 count=1 conv=notrunc

使用 dd 命令来操作 硬盘镜像文件 (hd60M.img) ,它的作用是将 mbr.bin 文件写入 hd60M.img 的前 512 字节,通常用于 写入主引导记录(MBR, Master Boot Record) 。下面是具体解释:


命令参数解析

参数含义
dd数据复制命令,用于低级别复制和转换数据。
if=./mbr.bin输入文件 (if = input file),这里是 mbr.bin,通常是一个 512 字节的主引导记录(MBR)二进制文件。
of=./hd60M.img输出文件 (of = output file),这里是 hd60M.img,即 60MB 的虚拟磁盘映像文件。
bs=512设定块大小为 512 字节(与 MBR 大小一致)。
count=1只复制 1 块(即 512 字节),刚好覆盖 MBR 区域。
conv=notrunc不截断输出文件,即保持 hd60M.img 的原始大小,仅覆盖前 512 字节。

输出如下图

image.png

再次启动bochs, 成功如下图

image.png

第一章算是成功完结

后续是本章我在阅读时候的一些笔记

. 驱动程序的作用

  • 什么是驱动程序
    • 驱动程序(Driver)是操作系统和硬件设备之间的桥梁。
    • 它是一段软件,允许操作系统与硬件设备进行通信,使得应用程序能够通过操作系统间接控制硬件。
  • 驱动程序的作用
    • 抽象硬件功能:不同的硬件有各自的物理特性和通信协议,驱动程序将这些硬件特性抽象为操作系统可以理解的接口。

    • 提供统一接口:驱动程序隐藏了硬件细节,为操作系统提供一致的操作接口。

    • 实现硬件的可用性:如果没有驱动程序,操作系统和软件无法直接使用硬件功能。

用户态和内核态区别

为什么内核态被称为管态?

  1. 完全控制硬件资源
    • 内核态可以直接操作CPU的寄存器、访问物理内存和外设。
    • 内核态允许执行诸如设置中断向量表、切换进程上下文等高级操作。
  2. 负责系统管理
    • 内核态是操作系统的核心运行环境,负责整个系统的监督与管理,包括:
      • 进程调度
      • 内存管理
      • 文件系统
      • 中断处理
      • 硬件控制
  3. “管态”的历史原因
    • 在早期计算机(如 IBM 大型机)的设计中,Supervisor Mode(监督模式) 是一种专门为操作系统设计的特权状态。
    • 随着计算机体系结构的演进,这一概念被保留并逐渐演变为现代的“内核态”(Kernel Mode),在中文中也称为“管态”或“核心态”。
  4. 安全性和隔离性
    • 管态(内核态)与用户态的划分是为确保系统的安全性和稳定性。
    • 用户程序无法直接进入管态,必须通过合法的入口(如系统调用)进入,这种机制保证了操作系统的核心不会被恶意程序破坏。

3. 管态的特权功能

在管态下,CPU可以执行以下特权操作:

  • 直接访问硬件设备
    • 控制外设,如磁盘、网络设备、显示器等。
  • 管理内存
    • 设置页表、切换地址空间。
  • 控制进程调度
    • 切换进程上下文,分配CPU时间片。
  • 处理中断
    • 接收硬件中断信号并调用对应的中断服务程序
用户态程序的内存访问受限
  • 不能直接访问物理内存

    • 物理内存的分配是由内核(管态)控制的,用户态程序无法直接操作物理地址空间。
    • 现代操作系统使用 虚拟内存(Virtual Memory) 机制,用户态程序只能访问操作系统为其分配的虚拟地址空间,而真实的物理内存管理是内核的职责
  • 用户态程序运行在受限模式下

    • 用户态程序没有权限访问或修改 MMU(内存管理单元) 的配置,例如不能直接修改 页表(Page Table)

    • 任何试图访问未被操作系统分配的内存地址(例如越界访问)都会触发 访问异常(Segmentation Fault)

内存分段和平坦模型

以下是它们的联系与区别:


1. 什么是内存分段(Memory Segmentation)?

  • 定义
    • 内存分段是一种将内存划分为若干段(Segment)的机制,每个段代表一块逻辑区域,例如代码段、数据段、堆栈段等。
    • 每个段都有自己的基地址和大小限制,程序通过段选择器(Segment Selector)和段内偏移量(Offset)来访问内存地址。
  • 工作机制
    • CPU 使用段寄存器(如 CS、DS、SS 等)保存各段的基地址。
    • 内存地址通过段基地址 + 偏移量计算得出物理地址。
    • 分段模型强调逻辑地址到物理地址的映射。
  • 优点
    • 支持模块化程序设计,将代码、数据、堆栈等区域逻辑分离。
    • 方便内存的保护和权限控制,可以为不同段设置不同的权限(只读、可写等)。
  • 缺点
    • 段的划分和管理比较复杂,容易导致内存碎片。
    • 如果程序跨段访问,可能增加地址计算的复杂性。

2. 什么是内存平坦模型(Flat Memory Model)?

  • 定义
    • 内存平坦模型将整个内存视为一个线性连续的地址空间,不区分逻辑段。
    • 程序通过直接访问线性地址(Logical Address)与物理地址进行操作。
  • 工作机制
    • 平坦模型中,内存地址直接通过一个线性地址空间表示,不涉及段寄存器或段偏移的概念。
    • 所有的内存访问都是基于一个统一的地址空间进行。
  • 优点
    • 地址计算简单,方便程序开发和调试。
    • 消除了内存碎片问题,因为内存是统一连续的。
  • 缺点
    • 内存的保护和隔离机制较弱(需要其他技术如分页来配合实现)。
    • 不支持像分段模型那样的灵活分区管理。

3. 联系

  1. 都是内存管理的一种方式
    • 分段和平坦模型都是为了让程序更方便地访问内存,组织和管理内存地址。
    • 它们的目标都是高效利用内存资源。
  2. 可以结合使用
    • 在现代计算机中,分段和分页(平坦模型的一部分)常结合使用。
    • 例如:x86 架构允许同时启用分段和分页功能,其中分页提供平坦的线性地址空间,而分段用于逻辑地址的划分。

4. 区别

特性内存分段内存平坦模型
内存组织方式将内存划分为若干逻辑段,每个段有独立的基地址和大小将内存视为一个连续的线性地址空间
地址计算通过段基地址 + 偏移量计算物理地址地址直接对应内存的线性地址
复杂性复杂,需管理段寄存器和偏移量简单,所有地址直接使用线性地址访问
内存保护每个段可以设置不同的访问权限(只读、可写等)本身没有保护机制(需要配合分页等技术)
程序设计程序需明确管理段的切换和访问程序设计更加直观,适合现代高层语言
主要应用场景早期操作系统(如 DOS)、嵌入式系统、x86保护模式现代操作系统(如 Linux、Windows 平坦模式)

5. 实例说明

(1) 内存分段模型

在 x86 保护模式下:

  • 内存被分为代码段(CS)、数据段(DS)、堆栈段(SS)等。
  • 程序通过 CS:IP(段地址 + 偏移量)访问代码,通过 DS:SI 访问数据。

(2) 内存平坦模型

在现代操作系统中(如 Linux):

  • 整个内存被看作 0 到最大物理地址的一个线性区域。所有进程的地址空间都是从 0 开始的线性地址,依靠 分页(Paging) 进行地址映射。
  • 程序通过虚拟地址访问内存,内存的分段和权限管理由分页机制和内核完成。

16位寄存器表示20位内存地址

方法1 CPU自己解决

段地址只保存物理地址的高 16 位,而后 4 位通过偏移量的低 4 位补充计算出来,从而形成完整的 20 位物理地址。这种机制是 8086 CPU 的内存分段模型的核心思想。


段地址和偏移量的组合机制

物理地址的计算公式为:

具体解析:

  1. 段地址的作用
    • 段地址由段寄存器(如 CSDSSS)保存,它表示物理地址的高 16 位部分。
    • 因为段地址以 16 字节为单位,所以段地址的每一位实际代表物理地址的 4 个 bit(左移 4 位)。
  2. 偏移量的作用
    • 偏移量由偏移寄存器(如 BXSIDI)或立即数指定,它用于补充物理地址的低 16 位部分。
    • 偏移量可以动态变化,从而灵活地访问段内的不同地址。
  3. 物理地址的组合
    • 将段地址左移 4 位(乘以 16),作为物理地址的高位部分。
    • 偏移量直接加到段地址的结果上,补充物理地址的低位部分。

示例分析

示例 1:段地址与偏移量的简单组合

假设段地址为 0x2000,偏移量为 0x0034

  • 段地址: 表示物理地址的高 16 位。
  • 偏移量: 表示物理地址的低位补充。
  • 物理地址

示例 2:跨段访问

如果偏移量超过 64KB 的范围,需要切换段地址:

  • 假设当前段地址为 0x3000,偏移量为 0xFFFF
  • 如果偏移量再增加 1,就需要调整段地址:
    • 新的段地址可能是 0x4000,偏移量重新从 0x0000 开始。

方法2 使用编码方式解决

这种方法的核心是利用段地址和偏移量的组合,并通过立即数直接作为偏移量,避免偏移量必须从寄存器中读取

保护模式 和 内核模式,它们分别属于 CPU的运行模式 和 操作系统的运行特权 的概念。以下是详细解答:


1. CPU什么时候会进入保护模式?

CPU进入保护模式通常发生在 系统启动过程中,以下是具体情况:

  1. 启动流程概述
    • CPU加电启动时,默认运行在 实模式(Real Mode)。这种模式是为了兼容早期的x86架构,允许访问1MB内存。
    • 操作系统的引导程序(Bootloader)负责完成从实模式到保护模式的切换。
  2. 进入保护模式的步骤
    • 设置控制寄存器(CR0)
      • 向控制寄存器 CR0 的第 0 位(保护模式启用位,PE)写入 1,启用保护模式。
    • 加载全局描述符表(GDT)
      • 在进入保护模式之前,操作系统会初始化 GDT(全局描述符表),为内存分段和权限管理提供基础。
    • 切换代码段寄存器(CS)
      • 切换 CS(代码段寄存器)以指向保护模式下的有效段描述符。
    • 启用分页(可选)
      • 保护模式支持分页机制(通过 CR3 寄存器设置页表),这一步通常由操作系统完成。
  3. 何时进入保护模式?
    • 现代计算机在系统启动时
      • 在操作系统加载之前,Bootloader(如GRUB)会将 CPU 切换到保护模式。
    • 从实模式切换到保护模式
      • 当系统需要超过 1MB 的内存访问或需要多任务和虚拟内存功能时。

2. 保护模式与内核模式的区别

这两个术语涉及不同的层面:

特性保护模式内核模式
概念层面CPU 的硬件运行模式操作系统的运行权限级别
目标管理内存访问、支持多任务和虚拟内存提供对硬件资源和敏感操作的访问控制
运行范围CPU 启动保护模式后,所有进程运行在保护模式下操作系统的内核运行在内核模式,应用运行在用户模式
硬件支持使用段寄存器(如 CS、DS)和 GDT/TSS 等硬件机制通过 CPU 提供的特权级别(Rings)区分权限
切换方式通过设置 CR0 的 PE 位进入保护模式通过系统调用(如 syscall 或 int 指令)进入内核模式

保护模式下的持续运行

一旦 CPU 进入 保护模式(Protected Mode) 后,通常操作系统会继续运行在这个模式下。这是因为保护模式相比于实模式有以下显著优势:

  1. 支持更大的内存空间:保护模式允许访问 4GB 或更多内存(通过扩展的地址总线)。
  2. 多任务支持:保护模式支持硬件级别的多任务机制,可以运行多个程序并通过操作系统管理资源。
  3. 安全性:保护模式引入了分段和分页机制,为程序提供内存保护,防止程序之间互相干扰。
  4. 增强的指令集:保护模式支持更多的 x86 指令集。

因此,现代操作系统(如 Windows、Linux)会一直运行在保护模式下,除非有特殊需求。


程序分段 

,这三个段(代码段数据段BSS段)在现代计算机和操作系统中仍然广泛使用,尤其是在编译和链接过程以及可执行文件(如 ELF 文件)的内存布局中。这些段的用途如下:

1. 代码段(Code Segment)

  • 现状:依然被使用。
  • 功能:存储程序的可执行指令(只读)。
  • 特点
    • 通常标记为只读和可执行,以保护代码不被修改。
    • 在现代安全机制下,操作系统通过启用**不可执行内存(NX bit)**确保只有代码段可以被执行,防止代码注入攻击。

2. 数据段(Data Segment)

  • 现状:仍然使用。
  • 功能:存储已初始化的全局变量和静态变量。
  • 特点
    • 数据段的内容在程序启动时就加载到内存中。
    • 它通常可读写,用于程序执行期间需要的全局变量。

3. BSS段(BSS Segment)

  • 现状:依然存在。
  • 功能:存储未初始化的全局变量和静态变量。
  • 特点
    • BSS 段在内存中被初始化为 0,节省了存储空间,因为它不会占用磁盘文件空间,只在运行时分配。
    • 现代编译器和链接器仍然会将未初始化的变量分配到 BSS 段。

**
**

解读汇编

下面是对上述汇编代码的理解

jmp 0x90:start 的作用

  • 目的
    • 这条指令是一个远跳转(far jump) ,直接修改了段寄存器 CS 和指令指针 IP 的值。
    • 将程序的执行流跳转到代码段的入口地址,并同时初始化 CS 寄存器为 0x90
  • 背景
    • 为什么是 0x90
      • 程序在加载到内存时,被 MBR 放在物理地址 0x900(即内存的某个区域)。

      • 在实模式下,段寄存器 CS 的值是段基址,指向实际物理地址的 段基址 × 16。因此:
        0x90 × 16 = 0x900

      • 这条指令将 CPU 的代码段寄存器设置为 0x90,使得指令可以正确地从 0x900 处开始执行。

解释DS  数据段基质 

mov ax, section.my_data.start 

add ax, 0x900 

 这两行代码的意思,把数据段的段起始地址加载到ax这个寄存器,同时这个寄存器是16位,然后把0x900 这个代表整个代码起始地址添加到ax这个寄存器中,所以,在ax 中存储的是数据段的物理地址;

  • 如果使用 add ax, 0x90,那么计算结果将是错误的,因为 0x90 是段基址,而不是物理地址。

  • 段基址本身并不能直接参与内存地址的计算,它需要通过 段基址 × 16 转换为物理地址。而 0x900 已经是物理地址。

shr ax, 4 

mov ds, ax

是为了满足段寄存器(如 DS 数据段寄存器)的地址格式要求,因为在上述计算中,已经在ax 得到数据段开始的物理地址,需要右移4位,获取段地址,存储到ds 

解释栈基址

先分析下面的代码

section my_stack align=16 vstart=0

    times 128 db 0

stack_top:

(1)section my_stack align=16 vstart=0

  • section my_stack
    • 定义了一个名为 my_stack 的栈段。
    • 该段被分配了一块独立的内存区域,用于存储栈数据。
  • align=16
    • 指定该段的起始地址是 16 字节对齐的,符合段寄存器对地址对齐的要求。
  • vstart=0
    • 指定该段的逻辑起始地址为 0。也就是说,该段内的第一个字节的偏移量为 0

(2)times 128 db 0

  • 这行指令分配了 128 字节的内存空间,并用 0 初始化所有字节。
  • 栈段内存的布局如下:
    • 从偏移量 0x0 到 0x7F 是这段分配的内存空间。

(3)stack_top:

  • stack_top 是一个标号,指向当前段(my_stack)的末尾。
  • 标号的位置是内存的偏移量 0x80(十六进制),也就是这段内存的最高地址;栈的最高有效地址是栈空间的最后一个字节的位置,也就是偏移量 0x7F;但是,现在栈内没有其他的元素,所以栈顶只能是0x80

**
**

之后,下面的stack segment init  就很好理解

**
**

mov ax, section.my_stack.start  ; 加载栈段的偏移地址

add ax, 0x900                   ; 加载实际物理地址

shr ax, 4                       ; 计算段基址

mov ss, ax                      ; 初始化栈段寄存器 SS

mov sp, stack_top               ; 初始化栈指针 SP,设置栈顶位置

**
**

其中,栈顶位置stack_top 就是之前的0x80

解释自定义数据段内容

这段代码定义了一个名为 my_data 的数据段,并在其中声明了两个全局变量 var1 和 var2。下面逐行解析其含义:


1. section my_data align=16 vstart=0

  • section my_data
    • 定义一个新的数据段,段名为 my_data
    • 这是一个逻辑段,用于存储程序中的全局变量和常量。
  • align=16
    • 指定该段的起始地址是 16 字节对齐的。这是为了满足 x86 实模式下段地址的对齐要求。
  • vstart=0
    • 表示该段的逻辑起始地址为 0
    • 即段内变量的偏移地址从 0x0 开始计算。

2. var1 dd 0x1

  • var1
    • 声明了一个名为 var1 的变量。
    • dd(define double word)
      • 定义一个占用 4 字节(32 位)的变量。
      • 对应汇编语言中常用的数据类型,适用于存储 32 位整数或指针。
    • 0x1
      • 初始化 var1 的值为 0x1
  • 存储细节
    • 假设段起始偏移地址为 0x0,则 var1 的偏移地址为:

      var1 的偏移地址 = vstart + 0x0 = 0x0
      
    • 它占用 4 字节的内存,从 0x0 到 0x3


3. var2 dd 0x6

  • var2
    • 声明了一个名为 var2 的变量。
    • 同样使用 dd 定义,占用 4 字节。
  • 存储细节
    • var2 的偏移地址紧跟在 var1 后面,从 0x4 开始。

      var2 的偏移地址 = vstart + 0x4 = 0x4
      
    • 它占用 4 字节的内存,从 0x4 到 0x7


4. 段的布局

这段代码定义的 my_data 段的布局如下:

偏移地址内容描述
0x00x00000001var1 的值
0x40x00000006var2 的值

整个数据段占用 8 字节的内存。


5. 使用示例

在程序中可以通过数据段寄存器(DS)和偏移地址访问这些变量:

读取 var1 的值

mov ax, ds:[0x0]   ; 从 DS 段偏移地址 0x0 读取 var1 的值

读取 var2 的值

mov ax, ds:[0x4]   ; 从 DS 段偏移地址 0x4 读取 var2 的值

**
**

在 x86 汇编中,数据宽度的单位有以下常见类型:

  • byte:8 位(1 字节)。

  • word:16 位(2 字节)。

  • dword(double word):32 位(4 字节)。

  • qword(quad word):64 位(8 字节)

push word [var2] 的含义

  • push 指令
    • 将数据压入栈。
    • 栈的增长方向是从高地址向低地址,因此执行 push 时,栈指针(SP)会减少。
    • 数据会写入栈段(由 SS:SP 确定的位置)。
  • word 的作用
    • 明确表示压栈的数据宽度是 16 位(2 字节)
    • 指令会从 var2 的地址中读取 16 位(2 字节) 数据,并压入栈中。
  • [var2]
    • 表示从变量 var2 的地址开始读取数据。

    • var2 是一个 dword(双字,4 字节)的变量,但这里通过 word 修饰符,指示只读取变量的 低 16 位

  • 在保护模式下,段地址 + 偏移地址 得到的并不是物理地址,而是线性地址

  • 如果启用了分页机制,线性地址会通过页表的转换,最终映射到物理地址。

  • 分页机制的目的是支持更大的地址空间和内存虚拟化,因此它是现代操作系统(如 Linux 和 Windows)的核心机制。

实模式和保护模式的地址计算区别

在实模式下:

  • 段基址 × 16 + 偏移地址直接得出物理地址,这个地址可以直接用于访问内存。
  • 实模式设计的初衷是向后兼容简单的16位环境(如 MS-DOS)以及早期的 x86 架构,这些环境下不存在虚拟地址的概念。

虚拟地址是为了支持操作系统的内存管理而引入的。实模式运行在 CPU 刚刚上电的初期,系统还没有进入保护模式,也没有复杂的操作系统参与,因此不具备虚拟地址的条件。

. 实模式下的地址计算

  • 在实模式下,内存地址由 段基址 和 段内偏移 通过以下公式直接计算得出:

    Copy code
    物理地址 = 段基址 × 16 + 段内偏移
    
  • 实模式下的地址计算非常简单,直接输出一个 物理地址,可以用来访问内存。

  • 如图所示:

    • 段基址为 0xC0,偏移地址为 0x01,计算出的物理地址为:

      Copy code
      物理地址 = 0xC0 × 16 + 0x01 = 0xC01
      
  • 结论

    • 在实模式下,段地址 + 偏移地址直接得到物理地址,无需分页或虚拟地址的转换。

2. 保护模式下的地址计算

  • 保护模式引入了分段机制和分页机制
    • 分段机制
      • 段地址由选择子索引到 GDT(全局描述符表),通过段描述符计算出段基址。
      • 段基址 + 偏移地址得到 线性地址
    • 分页机制(可选):
      • 如果分页功能开启,线性地址需要通过页表转换为物理地址。
      • 如图所示,如果分页功能已开启,则线性地址被认为是虚拟地址,需要经过页表转换为物理地址。
  • 结论
    • 保护模式下是否使用分页取决于是否启用了分页功能(由 CR0 寄存器的第 31 位控制)。

为什么在保护模式中,存储在寄存器中的段基址 会变成选择子selector ?

段选择子(Selector) 的确相当于全局描述符表(GDT)或本地描述符表(LDT)中的一个索引,通过它,CPU 可以快速定位段描述符

举一个例子, 在real mode 中完成GDT如下图

GDT 的定义和初始化可以在 Real Mode(实模式)完成,但它的使用是在保护模式(Protected Mode)中生效的

  • 软中断:软中断是通过软件触发的一种中断机制,通常由程序发起。比如 Linux 使用 int 0x80 指令(x86 架构)进入内核,从用户态切换到内核态,处理系统调用请求。

  • 硬中断:硬中断是由硬件触发的中断机制,例如键盘输入、鼠标点击或硬盘读写请求等。这些中断是由硬件直接发出信号给 CPU。

但是 栈支持LIFO

编译器的自举过程

(1)新功能的引入(支持转义符 \

  • 起点
    • 老编译器 compile_old.c 不能处理 \ 这种转义符。
    • 为了支持这一功能,开发者修改了编译器的源代码,生成了 compile_new_a.c
  • 问题
    • 老编译器虽然可以编译 compile_new_a.c,生成新的编译器 compile_new_a,但它无法正确处理支持 \ 的新特性。
    • 新功能需要进一步的编译器更新。
  • 结果
    • 用老编译器编译 compile_new_a.c,生成 compile_new_a,它可以支持 \

(2)迭代改进(支持换行符 \n

  • 在支持 \ 的基础上,再扩展支持 \n
  • 修改 compile_new_a.c,生成新的源码 compile_new_b.c,然后用支持 \ 的编译器(compile_new_a)去编译 compile_new_b.c
  • 问题
    • compile_new_a 可以识别 \,但无法直接识别 \n,需要使用其 ASCII 值(10)作为中间表示。
    • 通过这种方式,逐步完成对 \n 的支持。
  • 结果
    • 新的编译器版本 compile_new_c 生成后,可以直接识别 \n,功能更加强大。

编译型和解释性代码的区别

JS 脚本程序

    • 运行时会通过 Node.js 引擎 或 浏览器内核(如 V8 引擎)来解释执行。
    • CPU 看到的实际上是 Node.js 或 浏览器进程
    • 这些引擎会负责读取 JS 源代码,解释为机器码供 CPU 执行。
  • 编译型程序

    • 如果你用 C++ 写一个程序并编译成可执行文件,CPU 可以直接看到编译后的程序进程。

    • 操作系统调度器直接调度的是编译生成的二进制程序。

查询ubuntu 系统,发现居然是小端序列

假设有一个 16 位的数据 0x1234,存储到内存中时的字节排列:

(1)大端序存储方式:

  • 内存地址从低到高排列:第一个字节是 0x12,第二个字节是 0x34
  • 存储顺序:
    • 内存地址 0x00: 0x12
    • 内存地址 0x01: 0x34

(2)小端序存储方式:

  • 内存地址从低到高排列:第一个字节是 0x34,第二个字节是 0x12
  • 存储顺序:
    • 内存地址 0x00: 0x34

    • 内存地址 0x01: 0x12

大端序的优点:

  • 更符合人类阅读习惯,因为字节顺序和书写顺序一致。
  • 适合网络传输协议(例如 TCP/IP),因此很多网络协议使用大端序。 这个是重点

小端序的优点:

  • 在处理低位对齐时效率更高,例如从内存中读取 16 位或 32 位整数时,不需要调整地址。

  • 现代大多数计算机(如 x86)都采用小端序。

TCP IP 数据在进行网络传输,我觉得问题就出现在接收数据的时候没有转换成主机序列

  • 发送数据时

    • 将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order)。
    • 例如,将一个 32 位整数从小端序转换为大端序。
  • 接收数据时

    • 将网络字节序转换为主机字节序,方便主机解析。
如果传输的是字符串

**如果你发送的数据长度是 100 字节,你是否需要使用 htonl 或 ntohl 进行字节序转换,取决于数据的类型和内容。**以下是具体分析和推荐:


1. 发送的数据类型

  • 整数数据
    • 如果你的 100 字节数据中包含整数类型的数据(如 int32_tint16_t),那么这些整数需要进行字节序转换。
    • 例如:
      • 使用 htonl 转换 32 位整数(如 uint32_t)。
      • 使用 htons 转换 16 位整数(如 uint16_t)。
  • 字符串或纯字节数组
    • 如果数据是字符串(如 "Hello, World!")或者是任意的字节流,则不需要进行字节序转换。
    • 原因是字符串和字节流的每个字节在网络中传输时是独立的,不会受到字节序影响。

2. 发送 100 字节的数据

情景 1:如果数据是整数数组

例如,数据是由 25 个 32 位整数组成(每个占 4 字节,共 100 字节):

uint32_t data[25];

在发送之前,需要将每个整数从主机字节序转换为网络字节序:

for (int i = 0; i < 25; i++) {
    data[i] = htonl(data[i]);  // 转换每个整数
}
send(sockfd, data, sizeof(data), 0);

情景 2:如果数据是字符串或字节流

如果数据是字符串(如 "Hello, World!")或者未解释的字节流:

char data[100] = "Some binary or text data...";

这种情况下,不需要使用 htonl 或 ntohl,可以直接发送:

send(sockfd, data, sizeof(data), 0);



如果发送的数据是字符串或纯字节流,发送时不需要转换成网络字节序。在接收端,也不需要进行字节序转换,可以直接使用接收到的数据。

3. 如何判断是否需要字节序转换

  • 需要转换的情况
    • 数据中包含需要解析的多字节整数(如 IP 地址、端口号、文件偏移量等)。
    • 接收方需要以正确的字节序解析这些数据。
  • 不需要转换的情况
    • 数据是字符串、单字节字符数组,或者未解释的二进制数据(字节流)。