Bootstrap

李志军 x86指令架构 操作系统学习笔记

课程L1 L2

1. 何为实模式、保护模式?


参考链接:​​​​​​链接 1

 CPU的实模式和保护模式(一) - 知乎 

【构建操作系统】全局描述符表GDT - 知乎


小结:实模式,即直接访问物理实际地址,模型为

        段基址+左移4位 +段偏移

        形如 

        数据段+左移4位+ 通用寄存器值 = 数据地址
        栈段SS+左移4位 + SP = 栈地址

        之所以 左移4位,是因为 早期cpu寄存器是16位的,地址线有20位。直接存不下物理地址

        保护模式,cpu32位,地址32位,可以访问更大地址空间和进行段地址权限控制,模型改动为,段寄存器 存 全局描述符表(GDT)的表项的索引值。表项存段基址

2.   test.c   test.i  test.s  test.o  test.exe  ?

小结:

预处理阶段。编译器首先对源代码进行预处理,处理以“#”开头的预处理指令,如“#include”和“#define”。预处理器根据这些指令修改源代码,生成一个扩展名为“.i”的预处理后的文件。

编译阶段。在编译阶段,编译器将预处理后的文件翻译成汇编代码,生成以“.s”结尾的汇编文件。这一阶段包括词法分析语法分析语义分析中间代码生成、优化,最终生成与特定计算机体系结构相对应的汇编语言代码。

汇编阶段。汇编器将汇编代码翻译成机器代码,生成以“.o”结尾的目标文件。这一阶段的输出是二进制文件,其中包含可执行的机器指令。

链接阶段。链接器将目标文件和必要的库文件合并成一个完整的可执行文件。链接器负责解决外部函数和变量的引用,并确保不同的代码段(如指令和数据)正确地链接在一起。

即 test.i 称为预处理后的文件,test.s称为 汇编代码 具有段格式。test.o称为 链接可重定位文件 具有段格式,二进制格式。

3. 为什么分段及基本内存布局?


参考 《深入理解计算机》,“链接” 篇章

小结:最终的可执行文件 是分段的,数据段,代码段。分段的好处在与 将数据归类 便于查找 和加载到内存中。只是因为cpu 段寄存器的存在 导致了内存在逻辑上的分段,不存在物理分段。

分多个段 是因为 一个段偏移寄存器表示范围只有16位

P2小结:上电后 执行bios,加载bootseg后,bootseg将

1.setup及其剩余的操作系统代码加载到内存,

2.打印 启动日志,
3.将 控制权交到setup

 


实验二 :操作系统的引导






1.为什么 在屏幕上显示字符串要读取光标的位置?

参考:c获取光标位置_一步步编写操作系统 75 从显卡读取光标位置1-CSDN博客
即 显示字符串 本质上不需要读取光标位置,只跟显存有关,但加上这个操作是为了 引导用户看见

利用 BIOS INT 0x10 功能 0x03 和 0x13 来显示信息:“'Loading'+回车+换行”,显示包括
! 回车和换行控制字符在内共 9 个字符。
! BIOS 中断 0x10 功能号 ah = 0x03,读光标位置。
! 输入:bh = 页号
! 返回:ch = 扫描开始线;cl = 扫描结束线;dh = 行号(0x00 顶端);dl = 列号(0x00 最左边)。
!
! BIOS 中断 0x10 功能号 ah = 0x13,显示字符串。
! 输入:al = 放置光标的方式及规定属性。0x01-表示使用 bl 中的属性值,光标停在字符串结尾处。
! bh = 显示页面号;bl = 字符属性;dh = 行号;dl = 列号。cx = 显示的字符串字符数
! es:bp 此寄存器对指向要显示的字符串起始位置处。

2. 为什么 增加对es的处理?


答:es:bp 为读取的msg地址,设置为 #0x07c0(系统加载boot的默认位置),而非 #9000(boot将自己重复制到的位置) 是因为本实验的目的只涉及 打印。
boot的基本流程:
        Linux 的最前面部分是用 8086 汇编语言编写的(boot/bootsect.S),并保存在引导设备的第一个扇区 中。它将由 BIOS 读入到内存绝对地址 0x7C0031KB)处。当它被执行时就会把自己移动到内存绝对 地址 0x90000576KB)处,并把启动设备盘中后 2KB 字节代码(boot/setup.S)读入到内存 0x90200 处。 而内核的其他部分(system 模块)则被读入到从内存地址 0x1000064KB)开始处。

3. 为什么 设置引导扇区标志 ( .org508)?

 答:3.1  org的概念

   ORG 2000H   
   START:MOV  AX,#00H
汇编语言源程序中若没有ORG伪指令,则程序执行时,指令代码被放到自由内存空间的CS:0处;若有ORG伪指令,编译器则把其后的指令代码放到ORG伪指令指定的偏移地址。两个ORG伪指令之间,除了指令代码,若有自由空间,则用0填充。

        3.2  .word 的概念

答:举例来说, 
_rWTCON: 
.word 0x15300000 
就是在当前地址,即_rWTCON处放一个值0x15300000 

翻译成intel的汇编语句就是: 
_rWTCON dw 0x15300000 

        3.3  boot_flag的规定

 答:规定必须位于 引导扇区的最后两个字节
        3.4 暂不加载文件系统?


4. 复习 从指定扇区磁道读字节码

答:

BIOS 提供的访问磁盘的中断例程为 int 13h。读取 0面0道1扇区的内容到0:200的
程序如下所示。
mov ax,0
mov es,ax
mov bx,200h
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,2
int 13h 入口参数:
第17章 使用 BIOS进行键盘输入和磁盘读写
(ah)=int 13h的功能号(2表示读扇区)
(al)=读取的扇区数
(ch)=磁道号
(cl)=扇区号
(dh)=磁头号(对于软盘即面号,因为一个面用一个磁头来读写)
(dl)=驱动器号 软驱从0开始,0:软驱 A,1:软驱 B;
硬盘从 80h开始,80h:硬盘C,81h:硬盘D
es:bx 指向接收从扇区读入数据的内存区
返回参数:
操作成功:(ah)=0,(al)=读入的扇区数
操作失败:(ah)=出错代码


5. rol指令

rol,汇编语言指令,功能是把目的地址中的数据循环左移COUNT次,每次从最高位(最左)移出的数据位都补充到最低位(最右),最后从最高位(最左)移出的数据位保存到CF标志位。


6. bootsect.s 在当前页光标处打印字符的核心代码

! 读取光标位置
mov ah,#0x03 !三号功能
xor bh,bh !参数传递 第0面
int 0x10

! 显示字符串到光标位置
mov cx,#36 !显示多少个字符
mov bx,#0x0007 !bh 显示页面号,bl 字符属性
mov bp,#msg1
mov ax,#0x07c0
mov es,ax !es:bp 为读取的位置
mov ax,#0x1301 !ah=0x13 对应功能号,al=0x01为启用bl
int 0x10

inf_loop:
    jmp inf_loop

msg1:
    .byte 13,10
    .ascii "Hello os world, my name is dcw"
    .byte 13,10,13,10
.org 510 !boot_flag必须位于引导扇区最后两个字节
boot_flag:
    .word 0xAA55

最后效果



7.从磁盘中读取setup.s到内存,并跳转setupseg

load_setup:
    mov dx, #0x0000 !设置设备号 和 磁头(那一面)
    mov cx, #0x0002 !设置扇区号 和 磁道号
    mov bx, #0x0200 ! es:bx 为写入内存位置,BOOTSEG + 512字节
    mov ax, #0x0200 + SETUPLEN !ah=0x02功能号,al = 0x02 读取的磁道数
    int 0x13
    jnc ok_load_setup

ok_load_setup:
    jmpi 0,SETUPSEG
    mov dx, #0x0000
    mov ax, #0x0000 ! 复位软驱重新读取
    int 0x13
    jmp load_setup

 setup.s 可暂时只打印字符 同 早期bootseg.s
效果图

8. 后续内容为 在setup.s 打印硬件参数 核心代码 为 16进制为字符转换、在光标处打印字符,与上雷同 可暂时略过。


课程L3 L4 L5

1.为什么跳转到systemseg 前启动保护模式?


答:因为保护模式的 寄存器访存 为32位,约4GB。比实模式 16位左移 加 偏离为 20位 约 1MB大得多。


2. 根据gdt表项寻址

答:

2.1 在保护模式仍为 段基址 + 偏移,不过段基址保存在gdt表项中。段寄存器表示 表项的索引。

2.2 段寄存器 为 16位,在保护模式下的含义如下:


则 jmpi 0,8 即 8:0, 0x8, 高13位表示index = 1

2.3 如gdt表项图 段基址 共占 32位,需要对号入座后需要拼接在一起,

limit_low = 0x07FF

base_low  = 0x000000 前24位

base_high = 0x00
2.4 则最后的线性地址为 0x00000000 + 0x0000


3.  宏展开

 答: 

 参考 C语言之宏详解(超级详细!)_c语言宏-CSDN博客

 4. 内联汇编

答:参考 C语言内联汇编-CSDN博客  

实验3 系统调用

1.对设置IDT表项的内联汇编的解释

答: 

__asm__ (

"movw %%dx,%%ax\n\t" \

 "movw %0,%%dx\n\t" \

"movl %%eax,%1\n\t" \

 "movl %%edx,%2" \

: \

: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \    0x8000 + 0x6000 + 0x0f00

"o" (*((char *) (gate_addr))), \                              &idt[0x80]

 "o" (*(4+(char *) (gate_addr))), \                         &idt[0x80] + 4

"d" ((char *) (addr)),"a" (0x00080000))                edx = &systemcall, eax = 0x00080000

1.
edx = &systemcall, eax = 0x00080000
2.

ax = dx = &systemcall的低16位

dx = 0x8000 + 0x6000 + 0x0f00

&idt[0x80] = eax,表项的低32位

&idt[0x80] + 4 = edx,4字节 即表项的高32位。

 3.

由此可知idt[0x80]被置为:
0~15位,偏移值低16位,&systemcall的低16位
16~31位,段选择子,0x0008
32~47位,属性值,0x0x8000 + 0x6000 + 0x0f00,其中p = 1, dpl= 11

48~63位,偏移值高16位,0x0000

 2.系统调用小结:

为什么调用系统api函数时,需要调用触发int 0x80?
答: 从上文 初始化idt表项可知,会将

1.dpl改为3

 2.段选择子置为0x8,则cpl 为0,

3. 段偏移为 systemcall.s 的地址 ,则可gpt寻址到内核代码systemcall了。

4. systemcall.s里  就会根据 触发int 0x80的同时 在eax中保存的系统调用号,查找数组,执行到最终的 系统函数了。

3. 添加系统函数的步骤

答: 从系统调用原理可知

1. 编写api,在对应头文件的添加 宏定义

如 在linux-0.11/lib下添加test.c

在  include/unistd.h中添加宏定义

2. 实现 函数功能,并修改 system_call对应数组 

修改include/linux/sys.h 中的system_call_table数组

实现函数

在 kenel下实现who.c

知识点:用户态和内核态传递数据,官方实验提示仅告知 从已有库函数抄

 函数实现如下

#include <asm/segment.h>
#include <errno.h>
#include <string.h>

char _myname[24];

int sys_iam(const char *name)
{
    char str[25];
    int i = 0;

    do
    {
        // get char from user input
        str[i] = get_fs_byte(name + i);
    } while (i <= 25 && str[i++] != '\0');

    if (i > 24)
    {
        errno = EINVAL;
        i = -1;
    }
    else
    {
        // copy from user mode to kernel mode
        strcpy(_myname, str);
    }

    return i;
}

int sys_whoami(char *name, unsigned int size)
{
    int length = strlen(_myname);
    printk("%s\n", _myname);

    if (size < length)
    {
        errno = EINVAL;
        length = -1;
    }
    else
    {
        int i = 0;
        for (i = 0; i < length; i++)
        {
            // copy from kernel mode to user mode
            put_fs_byte(_myname[i], name + i);
        }
    }
    return length;
}
 

3. 调用api测试

 这里 可引入api文件,或直接api里直接写main

oslab下添加

/* iam.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);
   
int main(int argc, char *argv[])
{
    /*调用系统调用iam()*/
    iam(argv[1]);
    return 0;
}

/* whoami.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>
#include <stdio.h>
   
_syscall2(int, whoami,char *,name,unsigned int,size);
   
int main(int argc, char *argv[])
{
    char username[64] = {0};
    /*调用系统调用whoami()*/
    whoami(username, 24);
    printf("%s\n", username);
    return 0;
}
 

实验4  进程运行轨迹的追踪和统计 

1.知识点 回顾

wait(&status): 回收所有子进程,返回子进程的状态,放在指针 &status 所指向的位置。
文件描述符:0 1 2 为固定文件描述符,dup(fd) 产生新的描述符,并指向同一文件。 

2. 进程调度

参考  【哈工大李治军】操作系统课程笔记4:CPU和多进程 + 【实验 4】进程运行轨迹的跟踪与统计实验_哈工大李治军老师操作系统笔记【4】csdn-CSDN博客

看懂sleep_on函数仅需要下面两张图

1.当前任务结构 加入链表的方式是:让链表头指针p指向自己,新产生的tmp(这样每个current对应一个tmp)指向旧的头(比当前任务先一步加入的任务)。

2.当 当前任务被唤醒后 会 返回继续执行 schedule()后面的 即163行后的,这样链表就会Last in Fisrt Out的依次被唤醒。

看懂 interruptible_sleep_on()  下面这张图。

即  由于当前任务 是属于 可被除wake_up()外的函数唤醒的话,被唤醒后 执行 schedule()后面的代码前  会检查 头指针是否等于 当前结构指针(谁执行当前代码 全局变量current就指向谁),若不是 就表明 我处在链表中间了,应该重新沉睡,等待被顺序唤醒。

看懂 调度函数 schedule

确实是  找到就绪状态 中 剩余时间片的任务 继续执行,即任何进程 只有当被唤醒(即状态被修改为 0/Task_running) 其时间片最大 才会继续执行。
 

看懂 主动睡眠sys_pause() 等

即 进入睡眠 只需要 两行代码 : 修改状态 ,调度函数。 
之所以 sleep_on 还要额外 进入 阻塞队列中,是因为被动进入睡眠 是 需要按顺序唤醒的。

L12

1. 用户栈 内核栈,以及如何相互切换(五段论) ?

内核栈:

         pc为模型机概念 为 cs:ip计算后得地址。

        EFLAGS 标志寄存器

用户栈 =>内核栈,int0x80,压入固定得五个ss,sp,eflags,pc,cs

内核栈=>用户栈,iret 恢复中断前得状态 与 int指令 搭配。依次弹出这个五个

TCB切换,内核栈esp,TCB的概念 线程的五个信息。

L13

1. 时钟中断?

答: 定时中断信号可以由 系统定时器硬件产生,也可由进程的定时器产生。 

参考 定时器与时钟中断 - 简书

2. PCB结构中的 元素 tss? eip ? esp? 

答:

0. eip 保存了 父进程的 int0x80(sys_fork)后面的指令地址,则切换到子进程时就会从 fork()后执行。

1.tss包含了 如下图的寄存器内容,现在switch_to 是tcb.esp的直接切换,因为tss切换IO太多

2. 创建线程的时候 设置tss,设置tss.esp0 内核栈 对应tcb,tss.esp用户栈

3.tss 小结:为进程的上下文信息,保存了几乎所有寄存器的信息。PCB的一部分。PCB具体结构参考 进程控制块:PCB之task_struct_tss与pcb-CSDN博客

4.tss 与 tcb 关系 => 线程与进程的关系:

即 早期只有 “task_struct”这个概念,  pcb 这个任务既有 内存cpu资源 又有 任务切换, 线程理解为 共享进程资源的子任务 用tcb 表达。 猜想 可以把刚创建的进程 等价一个tcb(该进程就一个子任务 == 主任务)

实验5 基于内核栈切换的进程切换

1. 实验提示切换内容:

        依次完成 PCB 的切换: current 指向下一个进程

        TSS 中的内核栈指针的重写:由于共享同一个tss,则将一个进程的esp0 置过来 

        内核栈的切换: 寄存器 esp 指向下一个进程的esp. 

        LDT 的切换: 段表的切换

        PC 指针(即 CS:EIP)的切换:即iret,执行下一个进程的用户代码,与reschedule成对出现的 ret_from_sys_call完成了这件事
        

2. 回顾五段论 具体需要切换哪些内容

核心就是 切换内核栈esp,然后iret 去支持用户代码。段表、页表等寻址资源也得切换,当然此版本未提及页表的概念。

 L16~L19 信号量

讲的不好,不如参考《深入理解计算系统》相关章节:

1.从进度图的角度,论证了信号量如何保护线程安全的/互斥锁 
2.在生产者 消费者模型中,除了引入了mutex信号量表示互斥,还引入了 货物数,剩余槽数作为信号量表示 立即通知 对立线程去工作 而非随机通知。

 L22 多级页表思想

问题:绝大部分页表项 不会被使用,若直接去掉,则会导致 二分查找不如 连续时直接偏移定位,但不去又占内存空间。

方案:多级页表 每一小块之间是连续的。形如:弃掉未被使用的表项,分成连续的小块。

 

实验6 地址映射与共享 

明白 段基址 + 段内偏移 = 虚拟地址    虚拟地址 = 页目录 + 页号+ 偏移 , 段基址 存在于段表LDT中。即地址翻译过程即可。可参考 《深入理解计算机》9.6节
 

L27 L28 键盘 屏幕等外设 

基本概念:1.操作系统 将其统一封装为文件(open, write, read),2.从底层汇编指令 即 (in 端口,数据/寄存器),(out  端口,数据/寄存器)。
中间的封装的代码:偏向 设备驱动方向,可略。

L29 ~L30 生磁盘到文件的封装

可略


L31 L32  目录结构 与 文件系统
 

简要介绍了数据结构:多级索引结构,为了便于查找,树结构 节点仅保存下一级得索引号,inode数据单独存放

;