References:
《操作系统真象还原》,郑钢
《x86汇编语言:从实模式到保护模式》,李忠
《汇编语言(第3版)》,王爽
=========================================================================================
Linux中90%以上的代码都是用在资源管理、策略、算法及数据结构等方面。操作系统受制于硬件的支持, 很大程度上它的能力取决于硬件的能力,很多操作都是硬件自动完成的。比如,处理器进入0特权级时, 会自动在任务状态段TSS中获得0特权级的栈地址。因此,要想全面理解操作系统,不仅需要了解上层软件的算法、原理、实现, 还要了解很多硬件底层的内容。
本项目实现的mini操作系统,包含:
1)内核线程、特权级变换、进程、任务调度、fork和exec、父子进程间的通信等;
2)内存管理、文件系统、管道、shell等;
3)锁、信号量等。
=========================================================================================
BIOS
CPU的硬件电路被设计成只能运行内存中的程序(内存比较快)。
在开机加电的一瞬间,CPU的cs:ip寄存器被强制初始化为0xF000:0xFFF0(BIOS入口地址),此处16字节的内容是跳转指令"jmp f000:e05b"(BIOS程序起始地址)。BIOS的主要工作是:
1)检测、初始化硬件,硬件自己提供了一些初始化的功能调用,BIOS直接调用;
2)建立中断向量表IVT,这样就可以通过"int 中断号"来实现相关的硬件调用。这些功能的实现也是基于对硬件的IO操作。不过在保护模式下,中断向量表已经不存在了,取而代之的是中断描述符表IDT(Interrupt Descriptor Table)。
文本显示:0xB 8000起始的32KB内存区域是用于文本显示,往0xB 8000处输出的字符会直接落到显存中,显存中有了数据,显卡会自动将其搬到显示器屏幕上。
; FILE: boot/mbr.asm
; 截取部分代码
mov ax, 0xb800
mov gs, ax
; 打印字符串
mov byte [gs:0x00], 'M'
mov byte [gs:0x01], 0xa4 ; 显示属性
=========================================================================================
MBR
Master Boot Record, 主引导记录, 位于0x7c00。0x7C00,是BIOS把MBR加载到内存后自动跳转过去的地址。
功能:从硬盘指定位置处加载 loader, 并跳转。
; FILE: boot/mbr.asm
; 截取部分代码
LOADER_BASE_ADDR equ 0x900 ; 自定义loader被加载到物理内存位置
LOADER_START_SECTOR equ 0x02 ; 自定义loader位于硬盘的扇区号
; 读取loader程序
mov eax, LOADER_START_SECTOR ; 起始逻辑扇区号,LBA28编址
mov bx, LOADER_BASE_ADDR ; 要写入的目标地址
mov cx, 4 ; 要读入的扇区数,这里设为4个扇区
call read_hard_disk_0
jmp LOADER_BASE_ADDR + 0x100 ; loader.bin文件头部偏移256字节
times 510-($-$$) db 0
db 0x55, 0xaa
=========================================================================================
LOADER
功能:
1)调用BIOS中断获取内存大小;2)构建GDT,开启保护模式;3)加载kernel;
4)构建页目录表和页表,开启分页机制;5)解析kernel的ELF,将ELF文件中的段segment拷贝到各段自己被编译的虚拟地址处;
6)跳转
--------------------------------------------
1)调用BIOS中断获取内存大小
调用BIOS中断0x15获取内存大小,并将其值存放在 loader.bin头部(地址0x900),内核将会从该位置读取内存大小(kernel/memory.c mem_init())。
2)构建GDT,开启保护模式
; FILE: boot/loader.asm
; 截取部分代码
; 构建gdt及其描述符
; 0号段描述符
gdt_0: dd 0x0000_0000
dd 0x0000_0000
; 1号代码段
gdt_code: dd 0x0000_ffff
dd DESC_CODE_HIGH4
; 2号数据段和栈段
gdt_stack: dd 0x0000_ffff
dd DESC_DATA_HIGH4
; 3号显存段
; 基地址0xb_8000, 段大小0xb_ffff-0xb_8000=0x7fff, 粒度4KB, 段界限0x7fff/4k=7
gdt_video: dd 0x8000_0007 ; limit=(0xb_ffff-0xb_8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; dpl为0
; gdt基址和界限值
gdt_size dw $-gdt_0-1
gdt_base dd gdt_0
; 加载gdt
; lgdt指令的操作数是一个48位(6字节)的内存区域,低16位是gdt的界限值,高32位是gdt的基地址
; GDTR, 全局描述符表寄存器
lgdt [gdt_size]
; 打开地址线A20
; 芯片ICH的处理器接口部分,有一个用于兼容老式设备的端口0x92,端口0x92的位1用于控制A20
in al, 0x92
or al, 0000_0010B
out 0x92, al
; 禁止中断
; 保护模式和实模式下的中断机制不同,在重新设置保护模式下的中断环境之前,必须关中断
cli
; 开启保护模式
; CR0的第1位(位0)是保护模式允许位(Protection Enabel, PE)
mov eax, cr0
or eax, 1
mov cr0, eax
; 清空流水线、重新加载段选择器
jmp dword sel_code:protcetmode_beginning
[bits 32]
protcetmode_beginning:
; ....
2.1 构建GDT。
2.2 加载GDT。lgdt指令,将GDT的基地址、界限值载入至GDTR寄存器。
2.3 打开地址线A20。
第21条地址线A20:实模式下,处理器访问内存的方式是将段寄存器的内容左移4位,再加上偏移地址,以形成20位的物理地址。实模式下,32位处理器的段寄存器的内容仅低20位有效,高20位全部为0(即,只能使用20根地址线)。故,处理器只能访问1MB内存。(回绕)
2.4 禁止中断。
在设置好保护模式下的中断环境之前,必须关中断(指令cli)。保护模式下的中断机制和实模式不同,原有的中断向量表IVT不再适用。而且,保护模式下,BIOS中断也不能再用,因为它们是实模式下的代码。
2.5 将CR0的PE位置1,开启保护模式。
控制实模式/保护模式切换的开关是CR0寄存器。CR0是处理器内部的控制寄存器(Control Register),是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。CR0的第1位(位0)是保护模式允许位(Protection Enable, PE),该位置1,则处理器进入保护模式,按保护模式的规则开始运行。
3)加载内核
; FILE: boot/loader.asm
; 截取部分代码
KERNEL_BIN_BASE_ADDR equ 0x70000 ; 自定义kernel被加载到物理内存位置
KERNEL_START_SECTOR equ 0x09 ; 自定义kernel位于硬盘的扇区号
mov ax, sel_data
mov ds, ax
; 加载kernel,从硬盘读取到物理内存
; 这里为了简单,选择了在开启分页之前加载
mov eax, KERNEL_START_SECTOR ; kernel.bin在硬盘中的扇区号
mov ebx, KERNEL_BIN_BASE_ADDR ; 从磁盘读出后,写入到ebx指定的物理内存地址
mov ecx, 200 ; 读入的扇区数
call read_hard_disk_0
4)构建页目录表和页表,开启分页机制
; FILE: boot/loader.asm
; 截取部分代码
PAGE_DIR_TABLE_POS equ 0x10_0000 ; 自定义页目录表基地址,1MB
mov eax, PAGE_DIR_TABLE_POS ; 页目录表PDT基地址
add eax, 0x1000 ; 4KB,此时eax为第一张页表的基地址
mov ebx, eax ; 为.create_pte做准备,ebx为基址
; 将页目录表第0和第0x300即768项都指向第一张页表, 为将地址映射为内核地址做准备
; 0x300项 * 每个页目录项对应4MB = 3GB
or eax, PG_US_U | PG_RW_W | PG_P ; 定义该页目录项的属性为用户属性,所有特权级别都可以访问
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项, 指向第一张页表的物理基地址
mov [PAGE_DIR_TABLE_POS + 0x300*4], eax ; 第0x300个目录项, 也指向第一张页表
; 一个页表项占用4字节, 0xc00表示第768个页表占用的目录项,0xc00以上的目录项指向内核空间
sub eax, 0x1000 ; 4KB,重新指向自定义的页目录PDT基地址
mov [PAGE_DIR_TABLE_POS + 1023*4], eax ; 使最后一个目录项指向页目录表自己的地址
; 骚trips, 用于后面修改页目录项和页表项
; 创建页表项(PTE)Page Table Entry
; 本项目的mbr、loader、内核都放置在物理内存的低端1MB内
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte:
mov [ebx+esi*4], edx ; ebx已赋值为0x10_1000, 即自定义的第一张页表基地址
add edx, 4096 ; 4KB
inc esi
loop .create_pte
; 页目录中创建内核其它页表的PDE ???
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二张页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000 ; 页大小为4KB
loop .create_kernel_pde
sgdt [gdt_size] ; 存储到原来gdt的位置
add dword [gdt_base], 0xc000_0000 ; 全局描述符表寄存器GDTR也用的是线性地址
lgdt [gdt_size] ; 将修改后的GDT基地址和界限值加载到GDTR
; 令CR3寄存器指向页目录
mov eax, PAGE_DIR_TABLE_POS ; 把页目录地址赋给控制寄存器cr3
mov cr3, eax
; 开启分页机制
; 从此,段部件产生的地址就不再被看成物理地址,而是要送往页部件进行变换,以得到真正的物理地址
mov eax, cr0
or eax, 0x8000_0000 ; 打开cr0的pg位(第31位),开启分页机制
mov cr0, eax
4.1 物理内存1MB之上:
第1个4KB, 为页目录表PDT
第2个4KB, 为创建的第一张页表(第0和第768(0x300)个页目录项都指向它)
第769~1022个页目录项共指向254个页表
最后一个页目录项(第1023个)指向页目录表PDT本身
因此,共256个页,正好1M。即,物理内存1MB之上的1MB已用于页目录表和页表。
4.2 控制寄存器CR3指向页目录表基地址
4.3 将CR0的PG位置1,开启分页机制
5)解析kernel的ELF,将ELF文件中的段segment拷贝到各段自己被编译的虚拟地址处
; FILE: boot/loader.asm
; 截取部分代码
KERNEL_BIN_BASE_ADDR equ 0x70000; 自定义kernel被加载到物理内存位置
; 遍历段时,每次增加一个段头的大小e_phentsize
mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移42字节处是属性e_phentsize, 表示program header大小
; 为了找到程序中所有的段,必须先获取程序头表(程序头program header的数组)
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移28字节是e_phoff, 表示第1个program header在文件中的偏移量
; 其实该值是0x34, 不过还是谨慎一点,这里来读取实际值
add ebx, KERNEL_BIN_BASE_ADDR ; 加上内核的加载地址,得程序头表的物理地址
; 程序头的数量e_phnum,即段的数量,
mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移44字节是e_phnum, 表示有几个program header
; 遍历段
.each_segment:
cmp byte [ebx + 0], 0 ; 若p_type等于0, 说明此program header未使用
je .PTNULL
; 为函数memcpy压入参数, 参数是从右往左依次压入
; 函数原型类似于 memcpy(dst, src, size)
push dword [ebx + 16] ; 压入memcpy的第3个参数size
; program header中偏移16字节的地方是p_filesz
mov eax, [ebx + 4] ; program header中偏移4字节的位置是p_offset
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址, eax为该段的物理内存地址
push eax ; 压入memcpy的第2个参数src源地址
push dword [ebx + 8] ; 压入memcpy的第1个参数dst目的地址
; program header中偏移8字节的位置是p_vaddr,这就是目的地址
call mem_cpy ; 调用mem_cpy完成段复制
add esp, 12 ; 清理栈中压入的三个参数
.PTNULL:
add ebx, edx ; ebx指向下一个program header
; dx为program header大小, 即e_phentsize
loop .each_segment
将ELF文件中的段segment拷贝到各段自己被编译的虚拟地址处,将这些段单独提取到内存中,这就是所谓的内存中的程序映像。分析程序中的每个段segment,如果段类型不是PT_NULL(空程序类型),就将该段拷贝到编译的地址中
6)跳转
; FILE: boot/loader.asm
; 截取部分代码
KERNEL_ENTRY_POINT equ 0xc0001500
mov esp, 0xc009f000 ; ???
jmp KERNEL_ENTRY_POINT
这里将kernel的入口定义为 0xc000_1500,对应的在编译内核kernel.bin时需要指定该地址。
ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin %.o
=========================================================================================
kernel
// FILE: kernel/main.c
// 截取部分代码
int main(void)
{
init_all(); // kernel/init.c 初始化所有模块
cls_screen();
console_put_str("[OS@localhost /]$ ");
// 主线程完成使命后退出
thread_exit(running_thread(), true);
return 0;
}
/* init进程 */
void init(void)
{
unsigned int ret_pid = fork();
if(ret_pid) // 父进程
{
int status, child_pid;
while(1) // init在此处不停地回收过继给它的子进程
{
child_pid = wait(&status);
printf("i am init, my pid is %d, i recieve a child, it's pid is %d, status is %d\n", child_pid, status);
}
}
else // 子进程
{
my_shell();
}
panic("ERROR: during init, should not be here");
}
其中,init是第一个启动的程序, pid为1, 后续的所有进程都是它的孩子。init是所有进程的父进程, 它还要负责回收所有子进程的资源。init是用户级进程, 因此要调用 process_execute() 来创建进程,这一步是在thread_init()中完成的。
中断和系统调用
本项目支持的中断有:时钟、键盘、硬盘、int 0x80(系统调用)。
中断
|---- FILE: kernel/interrupt.c idt_init()
| | 初始化中断
| | 构建IDT,这里IDT中的每一项都指向对应的一段汇编代码,再由汇编调用C语言中断处理函数
| | 初始化可编程中断控制器8259A,放开所需要的中断
| | 中断发生时,会根据IDTR中的IDT基地址+中断向量*8,跳转到对应的汇编代码
| |
| | idt_desc_init() 初始化中断描述符表 struct gate_desc idt[IDT_DESC_CNT]
| | 中断描述符中包含了中断处理程序所在段的段选择子和段内偏移地址
| | make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i])
| | idt[i]中的每一项都指向对应的一段汇编代码intr_entry_table[i], 再由汇编调用C语言中断处理函数idt_table[i]
| | exception_init() 初始化异常名称, 并注册通用的中断处理函数idt_table[i] = general_intr_handler
| | pic_init() 初始化8259A
| | 主片8259A上打开的中断有: IRQ0的时钟、IRQ1的键盘和级联从片的IRQ2, 其它全部关闭
| | 从片8259A上打开IRQ14的硬盘
| | asm volatile("lidt %0" : : "m" (idt_operand))
| | 指令lidt把IDT的界限值、基地址加载到IDTR寄存器
| |
| |---- FILE: kernel/core_interrupt.asm
| | | 模板intr_%1_entry重复展开并构成intr_entry_table[]
| |
| |---- FILE: lib/io.h
| | | 内联汇编实现的读写端口函数
| | | 凡是包含io.h的文件,都会获得一份io.h中所有函数的拷贝
| | | inline是建议处理器将函数编译为内嵌方式,即在该函数调用处原封不动地展
| |
| |---- 系统调用
| | | make_idt_desc(&idt[0x80], IDT_DESC_ATTR_DPL3, syscall_handler)
| | | 系统调用对应为0x80号中断, 中断处理程序为汇编syscall_handler, 再调用中断处理函数syscall_table[]
| | |
| | |---- FILE: kernel/core_interrupt.asm syscall_handler 系统调用统一入口
| | |---- FILE: user/syscall-init.c syscall_table[] 每个系统调用对应的中断处理程序
| | |---- FILE: lib/syscall.h SYSCALL_NR 系统调用号
| | |---- FILE: lib/syscall.c 每个系统调用对外(用户)的接口
内存管理
这里先从指定位置处读取LOADER写入的物理内存大小。本项目中,物理内存的配置为32M(bochs配置文件bochsrc.cfg中"megs: 32"),减去低端的1MB、减去LOADER开启分页机制时创建PDT和PT占用的1MB(紧邻低端1MB之上),还有30MB,内核和用户内存池各占15M。所以,内核物理内存池的起始地址为 0x20_0000(2MB)。
4GB虚拟地址空间中,高1GB为内核空间,其中1GB之上的1MB虚拟空间已在LOADER阶段映射到物理内存的低端1MB。所以,内核虚拟地址池的起始地址为0xc010_0000(1GB+1MB)。
以页(4KB)为单位的内存管理,采用bitmap(位图)技术。本项目中,自定义内核物理内存的bitmap存放于0xc009_a000,自定义内核主线程栈顶为0xc009_f000、内核主线程PCB为0xc009_a000。所以,本系统最大支持4个页框的位图(一个页框大小的位图可表示128M内存,4个页框即512M),用于内核/用户物理内存池bitmap、内核虚拟地址池bitmap。
// FILE: kernel/memory.c
// 截取部分代码
/* 内存池结构,用于管理内存池中的所有物理内存 */
struct pool{
struct lock lock; // 申请内存时互斥, 避免公共资源的竞争
struct bitmap pool_bitmap; // 内存池用到的位图结构,用于管理物理内存
unsigned int phy_addr_begin; // 内存池所管理物理内存的起始地址
unsigned int pool_size; // 内存池字节容量,本物理内存池的内存容量
};
/* 虚拟地址池结构 */
struct virtual_addr{
struct bitmap vaddr_bitmap; // 虚拟地址用到位图结构
unsigned int vaddr_begin; // 虚拟地址起始值
};
/* 位图 */
struct bitmap{
unsigned int bitmap_bytes_len;
unsigned char *bits; // 位图所在内存的起始地址
};
struct pool kernel_pool, user_pool; // 内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 用于给内核分配虚拟地址
/* 初始化内存池 */
static void mem_pool_init(unsigned int mem_size)
{
// 伪代码
/* 初始化内核物理内存池、用户物理内存池 */
kernel_pool.phy_addr_begin = 2MB; // 起始地址
user_pool.phy_addr_begin = 2MB+15MB;
kernel_pool.pool_size = 15MB; // 内存池大小
user_pool.pool_size = 15MB;
kernel_pool.pool_bitmap.bitmap_bytes_len = 15MB/4KB/8bits; // 位图大小
user_pool.pool_bitmap.bitmap_bytes_len = 15MB/4KB/8bits;
kernel_pool.pool_bitmap.bits = (void *)0xc009_a000; // 指定位图起始地址
user_pool.pool_bitmap.bits = (void *)(0xc009_a000 + kbm_length);
bitmap_init(&kernel_pool.pool_bitmap); // 初始化位图
bitmap_init(&user_pool.pool_bitmap);
lock_init(&kernel_pool.lock); // 初始化锁
lock_init(&user_pool.lock);
/* 初始化内核虚拟地址池 */
kernel_vaddr.vaddr_bitmap.bitmap_bytes_len = kbm_length; // 与内核物理内存池大小一致
// 这里将其安排在紧挨着内核内存池和用户内存池所用的位图之后
kernel_vaddr.vaddr_bitmap.bits = (void *)(0xc009_a000 + kbm_length + ubm_length);
kernel_vaddr.vaddr_begin = 0xc010 0000; // 内核虚拟地址池的起始地址, 即3G+1M
bitmap_init(&kernel_vaddr.vaddr_bitmap); // 初始化位图
}
基于bitmap,实现了以页为单位的内存管理。 虚拟地址是连续的,但物理地址可能连续,也可能不连续。一次性申请count个虚拟页之后,再依次为每一个虚拟页申请物理页,并在页表中依次添加映射关联。
在以页(4KB)为单位的内存管理基础上,实现小内存块的管理,可满足任意内存大小的分配与释放(malloc/free)。这里采用arena模型。
/* 内存块描述符信息 */
struct mem_block_desc{
unsigned int block_size; // 内存块大小
unsigned int blocks_per_arena; // 每个arena可容纳此mem_blcok的数量
struct list free_list; // 空闲内存块mem_block链表
};
/* 内存仓库arena 元信息 */
struct arena{
struct mem_block_desc *desc; // 此arena关联的mem_block_desc
unsigned int count; // large为true时, count表示页框数; 否则, 表示空间mem_block数量
bool large; // 内存分配大于1024字节时为true
};
/* 内存块 */
struct mem_block{
struct list_elem free_elem;
};
// 内核内存块描述符数组
// 本系统支持7种规格的内存块: 16 32 64 128 256 512 1024字节
struct mem_block_desc k_block_descs[7];
内存管理系统
|---- FILE: kernel/memory.c mem_init()
| | 初始化内存管理系统
| | mem_pool_init() 初始化内存池: 内核虚拟地址池、内核/用户物理内存池
| | 虚拟地址池: 虚拟地址bitmap、虚拟地址池起始地址
| | 物理内存池:物理内存bitmap、物理内存起始地址、物理内存池大小
| | block_desc_init(k_block_descs) 初始化内核内存块描述符数组struct mem_block_desc k_block_descs[7]
| | 7种规格: 16 32 64 128 256 512 1024字节
| | 用户进程也有自己的内存块描述符数组, 定义在PCB中
| |
| |---- FILE: lib/bitmap.c bitmap_init() bitmap_scan() bitmap_set()
| | bitmap的基本操作
|
|---- FILE: kernel/memory.c malloc_page()
| | 页为单位的内存分配(基于bitmap技术)
| | 虚拟地址池中一次性申请count个虚拟页 vaddr_get()
| | 依次为每个虚拟页申请物理页, 并在页表中做映射
| | palloc() 在物理内存池中分配一个物理页
| | page_table_add() 页表中添加虚拟地址与物理地址的映射
| | 二级页表映射
| | pde_ptr(vaddr) 若页目录项不存在, 则先从内核空间申请一个物理页, 再将物理地址及属性写入PDE
| | pte_ptr(vaddr) 在虚拟地址对应的页表项中PTE写入物理地址及其属性
| |
| FILE: kernel/memory.c mfree_page()
| | 页为单位的内存释放
| | addr_v2p(vaddr) 获取虚拟地址对应的物理地址, 判断是内核/用户物理内存池
| | pfree(pg_phy_addr) page_table_pte_remove() 物理页挨个归还给物理内存池, 并清除虚拟地址所在的PTE
| | vaddr_remove() 一次性将连续的cout个虚拟页地址归还给虚拟地址池
| |
| |---- FILE: lib/bitmap.c bitmap_scan() bitmap_set()
|
|
|---- FILE: kernel/memory.c sys_malloc()
| | 任意内存大小的分配(基于arena模型)
| | 判断是内核线程还是用户进程, 再从块描述符数组struct mem_block_desc中匹配合适的规格
| | 若该规格的free_list为空, 申请一页内存作为arena, 再将arena拆分成该规格的内存块, 并添加到free_list
| | 内存分配: list_pop(&(descs[desc_index].free_list)
| |
| FILE: kernel/memory.c sys_free()
| | 任意内存大小的释放
| | 先将内存块回收到free_list:
| | struct mem_block *bk = vaddr
| | struct arena *ar = block2arena(bk)
| | list_append(&ar->desc->free_list, &bk->free_elem)
| | 再判断此arena中的内存块是否都空闲, 若是则释放arena
| | if(++ar->count == ar->desc->blocks_per_arena)
| | 将arena中所有的内存块从free_list中去掉, 释放arena(4KB页)
| |
| |---- FILE: lib/list.c 链表 list_empty() list_append() list_remove()
| |---- FILE: thread/sync.c 锁 lock_acquire() lock_release()
| |---- FILE: kernel/memory.c malloc_page() mfree_page() arena2block() block2arena()
物理地址/线性地址/虚拟地址/逻辑地址:
1)实模式下,"段基址+段内偏移地址"经过段部件的处理,直接输出的就是物理地址,CPU可以直接用此地址访问内存。
2)保护模式下,"段基址+段内偏移地址"为线性地址。但此处的段基址不再是真正的地址,而是一个选择子,本质上是个索引,类似于数组下标,通过这个索引便能在GDT中找到相应的段描述符。段描述符记录了该段的起始、大小等信息,这样便得到了段机制。若没有开启地址分页功能,此线性地址就被当作物理地址来用,可直接访问内存。
3)保护模式+分页机制,若开启了分页功能,线性地址则称为虚拟地址(虚拟地址、线性地址在分页机制下都是一回事)。虚拟地址要经过CPU页部件转换成具体的物理地址,这样CPU才能将其送上地址总线取访问内存。
逻辑地址,无论是在实模式或保护模式下,段内偏移地址又称为有效地址,也称为逻辑地址,这是程序员可见的地址。最终的地址是由段基址和段内偏移地址组合而成。实模式下,段基址在对应的段寄存器中(cs ds es fs gs);保护模式下,段基址在段选择子寄存器指向的段描述符中。所以,只要给出段内偏移地址就行了,再加上对应的段基址即可。
访问外部硬件有2个方式:
1)将某个外设的内存映射到一定范围的地址空间中,CPU通过地址总线访问该内存区域时会落到外设的内存中,这种映射让CPU访问外设的内存就如同访问主板上的物理内存一样。比如显卡,显卡是显示器的适配器,CPU不直接和显示器交互,只和显卡通信。显卡上有片内存叫显存,被映射到主机物理内存上的低端1MB的0xB 8000 ~ 0xB FFFF。CPU访问这片内存就是访问显存,往这片内存上写字节便是往屏幕上打印内容。
2)外设是通过IO接口与CPU通信的,CPU访问外设,就是访问IO接口,由IO接口将信息传递给另一端的外设。CPU从来不知道有这些设备的存在,它只知道自己操作的IO接口。
将数据和代码分开的好处:
1)可以赋予不同的属性,使程序更安全。如,数据,只读/只写/可读可写;代码,只读。
2)提高CPU内部缓存的命中率,使程序运行得更快。局部性原理,CPU内部有针对数据和针对指令的两种缓存机制。
3)节省内存。当一个程序的多个副本同时运行(比如同时执行多个ls命令),可以把只读的代码段共享,没必要在内存中同时存在多个相同的代码段。