目录
32位 DO_CALL (位于 i386 目录下 sysdep.h)
64位 DO_CALL (位于 x86_64 目录下 sysdep.h)
x86架构概述
计算机的工作模式
CPU 包括三个部分,运算单元、数据单元 控制单元
- 运算单元:负责计算
- 数据单元:暂存数据,包括缓存和寄存器组
- 控制单元:控制中心 取址执行
当前指令分两部分:一部分做什么操作 一部分操作那些数据
- 数据单元根据数据的地址,从数据段里读到数据寄存器里,就可以参与运算了。运算单元做完运算,产生的结果会暂存在数据单元的数据寄存器里。最终,会有指令将数据写回内存中的数据段。
总线上主要有两类数据
- 一个是地址数据,也就是我想拿内存中哪个位置的数据,这类总线叫地址总线(Address Bus);
- 另一类是真正的数据,这类总线叫数据总线(Data Bus)
数据总线的位数,决定了一次能拿多少个数据进来。
8086处理器原理
数据单元:
- 8086内部有8个16位的通用寄存器:AX、BX、CX、DX、SP、BP、SI、DI。
- 这些寄存器主要用于在计算过程中暂存数据。
- 这些寄存器比较灵活,其中 AX、BX、CX、DX 可以分成两个 8 位的寄存器来使用
- 这样,比较长的数据也能暂存,比较短的数据也能暂存。
控制单元:
IP 寄存器就是指令指针寄存器(Instruction Pointer Register).指向代码段中下一条指令的位置。
- -CPU 会根据它来不断地将指令从内存的代码段中,加载到 CPU 的指令队列中,然后交给运算单元去执行。
每个进程都分代码段和数据段,为了指向不同进程的地址空间,有四个 16 位的段寄存器。分别是CS、DS、SS、ES。
- -CS就是代码段寄存器,通过它可以找到代码在内存中的位置
- -CSDS 是数据段的寄存器,通过它可以找到数据在内存中的位置
- -CSSS是栈寄存器
- -CSES是附加段寄存器,存放当前执行程序中一个辅助数据段的段地址。 段寄存器 偏移地址寄存器
如果运算中需要加载内存中的数据,需要通过DS找到内存中 的数据,加载到通用寄存器中
- 对于一个段,有一个起始的地址,而段内的具体位置,称为偏移量(Offset),在CS和DS中都存着一个段的起始地址。
- 代码段的偏移量在IP寄存器中,数据段的偏移量会放到通用寄存器中。
起始地址都是16位的,IP寄存器和通用寄存器都是16位的,偏移量也是16位的,但8086的地址总线地址是20位
- 凑够20位方法:起始地址16+偏移量,
- 也就是把CS和DS中的值左移4位,变成20位,加上16位的偏移量,这样就可以得到20位的数据地址
从这个计算方式可以算出,对于只有20位地址8086来讲,能够分出的地址也就2^20=1M,超过这个空间就访问不到了。
- 如果你想访问1M+X的地方,这个位置已经超出20位了,由于地址总线只有20位,在总线上超过20位的部分是发不出去的,所以发出去的还是X,最后还是会访问1M内的X的位置。
那么一个段最大是多大,因为偏移量只能是16位的,所以一个段最大的大小为2^16=64K。
32位处理器
在32位的CPU中,有32根地址总线,可以访问2^32=4G的内存
x86架构是开放的,因此32位的CPU需要兼容原来的架构
在开放架构的基础上,如何保持兼容?
1. 通用寄存器 - 将8个16位的通用寄存器扩展到8个32位的通用寄存器,但依然保留16位和8位的使用方式
- - 高16位不能分成两个8位使用,因为这是不兼容的
2. IP寄存器 - 指向下一条指令的指令指针寄存器IP
- -会扩展成32位的,同样兼容16位
3. 段寄存器 - CS、DS、SS和ES仍然是16位,但不再是段的起始地址,段的起始地址放在内存的某个地方(表格)
- - 表格中的一项是段描述符,里面才是段真正的起始地址 - 而段寄存器里面保存的是这个表格中某一项,称为选择子
- - 获取段起始地址的流程:先间接地从段寄存器中找到表格中的一项,再从表格中的一项拿到段真正的起始地址
- - 为了快速拿到段的起始地址,段寄存器会从内存中拿到CPU的描述符高速缓存器中
- - 这种模式与8086的模式不兼容,但非常灵活,可以保持未来的兼容性
实模式 VS 保护模式
- 在32位的架构下,将前一种模式称为实模式(Real Pattern),后一种模式称为保护模式(Protected Pattern)
- 系统刚刚启动的时候,CPU处于实模式,此时和原来的模式是兼容的。即32位的CPU,也支持在原来的模式下运行,
- 当需要更多内存时,可以遵循一定的规则,进行一系列操作,然后切换到保护模式,就能够用32位CPU更强大的能力
- 如果不能无缝兼容,但通过切换模式兼容,也是可以接受的
系统交互
常用汇编指令
move a b :把b值赋给a,使a=b
call和ret :call调用子程序,子程序以ret结尾
jmp :无条件跳
int :中断指令
or :或运算
xor :异或运算
shl :算术左移
ahr :算术右移
push xxx :xxx入栈
pop xxx : xxx出栈
add a b :a=a+b
inc : 加1
dec : 减1
sub a b : a=a-b
cmp : 减法比较,修改标志位
从BIOS到bootloader
BIOS 时期
在主板上,有一个东西叫ROM(Read Only Memory,只读存储器)。
上面早就固化了一些初始化的程序,也就是BIOS(Basic Input and Output System,基本输入输出系统)。
在x86系统中,将1M空间最上面的0xF0000到0xFFFFF这64K映射给ROM
当电脑刚加电的时候,会做一些重置的工作,
将CS设置为0xFFFF,将IP设置为0x0000,所以第一条指令就会指向0xFFFF0,正是在ROM的范围内。
在这里,有一个JMP命令会跳到ROM中做初始化工作的代码,于是,BIOS开始进行初始化的工作
- 1.BIOS 要检查一下系统的硬件
- 2.要建立一个中断向量表和中断服务程序
- 3.检查正常则屏幕显示系统BIOS信息
bootloader 时期
BIOS初始化之后,我们需要操作系统。那么操作系统会在哪里呢?一般都会安装在硬盘上,在BIOS的界面上。你会看到一个启动盘的选项。一般在第一个扇区,占 512 字节,而且以 0xAA55 结束。
这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的代码(这个扇区通常称为MBR(Master Boot Record主引导记录 / 扇区)
1-BIOS完成任务后,会将boot.img从硬盘加载到内存中的0x7c00来运行
2-由于512字节实在有限boot.img做不了太多的事情。它能做最重要一个事情就是加载grub2另一个镜像core.img
- boot.img 先加载的是 core.img 的第一个扇区。如从硬盘启动,这个扇区里是diskboot.img,对应代码是 diskboot.S。
- boot.img 将控制权交给 diskboot.img 后,diskboot.img 的任务就是将 core.img 的其他部分加载进来,
------先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module对应的映像。
这里需要注意,它不是 Linux 的内核,而是 grub 的内核。
lzma_decompress.img 对应的代码是 startup_raw.S,本来 kernel.img 是压缩过的,现在执行的时候,需要解压缩。在真正的解压缩之前,lzma_decompress.img 需要调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西
从实模式切换到保护模式
第一项是启用分段,
- 就是在内存里面建立段描述符表,将寄存器里面段寄存器变成段选择子,指向某个段描述符,实现不同进程的切换
第二项是启动分页。
- 能够管理的内存变大了,就需要将内存分成相等大小的块
第三项打开 Gate A20,也就是第 21根地址线的控制线。
- (在实模式 8086 下面,一共就 20 个地址线,可访问 1M 的地址空间。如果超过了这个限度怎么办呢?当然是绕回来了。在保护模式下,第 21 根要起作用了,于是我们就需要打开 Gate A20。 )
- 切换保护模式的函数 DATA32 call real_to_prot 会打开 Gate A20,也就是第 21 根地址线的控制线
kernel.img 运行
kernel.img 对应的代码是 startup.S 以及一堆 c 文件
在 startup.S 中会调用 grub_main,这是grub kernel 的主函数。
正常启动,grub_main 最后会调用 grub_command_execute (“normal”, 0, 0),最终会调用 grub_normal_execute() 函数。
- 在这个函数里面,grub_load_config() 开始解析, grub.conf 文件里的配置信息。
- 在这个函数里面,grub_show_menu() 会显示出让你选择的那个操作系统的列表
- 一旦选定那个操作系统,就要开始调用 grub_menu_execute_entry()
开始执行选择的那一项,例如里面的linux16命令,表示装载指定的内核文件,并传递内核启动参数,
于是grub_cmd_linux()函数被调用,首先会读取linux内核头部的数据结构,加载到内存中来,检查通过,会加载整个linux内核镜像到内存
当都做完,调用grub_command_execute("boot",0,0),开始真正的启动内核。
启动过程总结
内核初始化
内核初始化, 运行 `start_kernel()` 函数(位于 init/main.c), 初始化做三件事
- - 创建样板进程, 及各个模块初始化
- - 创建【管理/创建用户态进程】的进程
- - 创建【管理/创建内核态进程】的进程
1-创建样板进程,及各个模块初始化 【各个职能部门的创建】
- 1- (项目管理初始化)创建第一个进程, 0号进程. ` set_task_stack_end_magic(&init_task) struct task_struct init_task = INIT_TASK(init_task)`
- 2- (办事大厅初始化)初始化中断, `trap_init()`. 系统调用也是通过发送中断进行, 由 `set_system_intr_gate()` 完成.
- 3- (会议室管理系统初始化) 初始化内存管理模块, `mm_init()`
- 4- (项目管理流程初始化) 初始化进程调度模块, `sched_init()`
- 5- (项目资料库初始化) 初始化基于内存的文件系统 rootfs, `vfs_caches_init()`
- 6- (其他初始化) 调用 `rest_init()` 完成其他初始化工作
0 号进程。这是唯一一个没有通过 fork 或者kernel_thread 产生的进程,是进程列表的第一个。
VFS(虚拟文件系统)将各种文件系统抽象成统一接口
2-用户态祖先进场创建-1号进程
创建【管理/创建用户态进程】的进程:1号进程
1- rest_init()通过 kernel_thread(kernel_init,...) 创建 1号进程(工作在用户态).
2- 权限管理
- x86 提供 4个 Ring 分层权限
- 操作系统: Ring0-内核态(访问核心资源); Ring3-用户态(普通程序)
3- 用户态调用系统调用:
- 用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态
4- 新进程执行 kernel_init 函数, 先运行 ramdisk 的 /init 程序(位于内存中)
- 首先加载 ELF 文件
- 设置用于保存用户态寄存器的结构体
- 返回进入用户态
- /init 加载存储设备的驱动
5- kernel_init 函数启动存储设备文件系统上的 init
3-内核态祖先进程的创建-2号进程
创建【管理/创建内核态进程】的进程: 2号进程
- `rest_init()` 通过 `kernel_thread(kthreadd,...)` 创建 2号进程(工作在内核态).
- `kthreadd` 负责所有内核态线程的调度和管理
总结
系统调用过程
glibc 对系统调用的封装
int open(const char *pathname, int flags, mode_t mode)glibc 里面的 open 函数
make-syscall.sh syscall-template.S
本节解析 glibc 函数如何调用到内核的 open
用户进程调用 open 函数
- - glibc 的 syscal.list 列出 glibc 函数对应的系统调用
- - glibc 的脚本 make_syscall.sh 根据 syscal.list 生成对应的宏定义(函数映射到系统调用)
- - glibc 的 syscal-template.S 使用这些宏, 定义了系统调用的调用方式(也是通过宏)
- - 其中会调用 DO_CALL (也是一个宏), 32位与 64位实现不同
32位 DO_CALL (位于 i386 目录下 sysdep.h)
- 1- 将调用参数放入寄存器中, 由系统调用名得到系统调用号, 放入 eax
- 2- 执行 ENTER_KERNEL(一个宏), 对应 int $0x80 触发软中断, 进入内核
- 3- 调用软中断处理函数 entry_INT80_32(内核启动时, 由 trap_init() 配置)
- 4- entry_INT80_32 将用户态寄存器存入 pt_regs 中(保存现场以及系统调用参数), 调用 do_syscall_32_iraq_on
- 5- do_syscall_32_iraq_on 从 pt_regs 中取系统调用号(eax), 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
- 6- entry_INT80_32 调用 INTERRUPT_RUTURN(一个宏)对应 iret 指令, 系统调用结果存在 pt_regs 的 eax 位置, 根据 pt_regs 恢复用户态进程
64位 DO_CALL (位于 x86_64 目录下 sysdep.h)
- 通过系统调用名得到系统调用号, 存入 rax; 不同中断, 执行 syscall 指令
- MSR(特殊模块寄存器), 辅助完成某些功能(包括系统调用)
- trap_init() 会调用 cpu_init->syscall_init 设置该寄存器
- syscall 从 MSR 寄存器中, 拿出函数地址进行调用, 即调用 entry_SYSCALL_64
- entry_SYSCALL_64 先保存用户态寄存器到 pt_regs 中
- 调用 entry_SYSCALL64_slow_pat->do_syscall_64
- do_syscall_64 从 rax 取系统调用号, 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用
- 返回执行 USERGS_SYSRET64(一个宏), 对应执行 swapgs 和 sysretq 指令; 系统调用结果存在 pt_regs 的 ax 位置, 根据 pt_regs 恢复用户态进程
系统调用表 sys_call_table
- 32位 定义在 arch/x86/entry/syscalls/syscall_32.tbl
- 64位 定义在 arch/x86/entry/syscalls/syscall_64.tbl
- syscall_*.tbl 内容包括: 系统调用号, 系统调用名, 内核实现函数名(以 sys 开头)
- 内核实现函数的声明: include/linux/syscall.h
- 内核实现函数的实现: 某个 .c 文件, 例如 sys_open 的实现在 fs/open.c
- .c 文件中, 以宏的方式替代函数名, 用多层宏构建函数头
- 编译过程中, 通过 syscall_*.tbl 生成 unistd_*.h 文件
- unistd_*.h 包含系统调用与实现函数的对应关系
- syscall_*.h include 了 unistd_*.h 头文件, 并定义了系统调用表(数组)
总结-64位系统调用分析