linux0.11内核源码阅读
基本上每天一个文件吧
RTFSC
有makefile https://github.com/cyysu/linux0.11/tree/master
无makefile https://github.com/beride/linux0.11-1
可编译调试的 https://github.com/yuan-xy/Linux-0.11
从服务器启动qemu无图形界面: -curses(在教室开虚拟机太耗电了,只能开云服务器,学校的网还卡艹)
- alt 2切换到监视 alt 1切换会模拟界面
- esc 2和esc 1 同样效果
vscode配置
launch.json
{ |
tasks.json
{ |
boot
bootsect.s
8086汇编 ljmp 0x7c0:5
将cs=0x7c0 ip=5 跳转到新的代码段和偏移地址
ok1_read: |
因为实模式下寄存器最多寻址64kb,所以需要判断是否需要修改段寄存器,这里我觉得比较有意思的是直接用0减去段内偏移,再把结果当作无符号数做运算得到需要读取扇区数
sub ax,bx
ax为0时相当于得到bx的补码也就相当于做了无符号运算0x1 0000-bx,由于寄存器位数限制无法直接操作0x1 0000,感觉这种方法确实好
setup.s
准备进入保护模式前先关了中断,这是因为此时idtr寄存器指向0,即bios的中断向量表,后面setup把system移动到0x0处,会覆盖掉这个表,中断也就g了
然后加载中断描述符表和全局描述符表
lidt fword ptr idt_48 ;// 加载中断描述符表(idt)寄存器,idt_48 是6 字节操作数的位置 |
lidt lgdt 前两个字节是描述符表的长度限制,后四个字节是描述符表的线性基地址
idt_48: |
gdt: |
第1个描述符00C0 9A00 0000 07FF 代码段描述符 基地址:0x0000 0000 DPL=0 非一致性代码段
第2个描述符00C0 9200 0000 07FF 数据段描述符 基地址:0x0000 0000 DPL=0 向上扩展的数据段
lmsw来给cr0的PE赋值启用保护模式标志,lmsw是为了兼容80286,386及以上推荐用mov cr0
mov ax,0001h ;// 保护模式比特位(PE)。 |
cs段选择子为8即000000001 0 00
- TI位为0 表示是全局描述符里的1个描述符
- RPL=0
跳转到0地址处system模块执行
head.s
程序所在物理地址为0,而且此时还没开启分页
lss esp,_stack_start ;// 表示_stack_start -> ss:esp,设置系统堆栈。 |
_stack_start定义在kernel/sched.c
long user_stack[PAGE_SIZE >> 2]; // 定义系统堆栈指针,4K。指针指在最后一项。 |
lss指令把stack_start 前四字节装入ESP寄存器,后两字节装入SS
重新设置了idt表和gdt表后(只是把表长变为了16M),因为前面0地址处的代码已经执行完了就没用了,所以用来做了页目录表
_pg_dir: ;// 页目录将会存放在这里。 |
并且在后面设置了4个页表来描述16M的物理内存
org 1000h ;// 从偏移0x1000 处开始是第1 个页表(偏移0 开始处将存放页表目录)。 |
stosb, stosw, stosd。这三个指令把al ax eax的内容存储到edi指向的内存单元中,同时edi的值根据方向标志的值增加或者减少
设置页目录项和页表具体操作是先把这5个表清零,然后把页目录表项前四项填上值
mov eax,_pg_dir |
设置页表项时倒叙从pag3最后一项开始
mov edi,pg3+4092 ;// edi -> 最后一页的最后一项。 |
把页目录的前四项这样去映射说明了这个任务的前16M线性地址等于物理地址
最后设置cr3指向物理地址0即页目录,然后设置cr0 pg位开启分页保护 ret到main函数
init
main.c
linux0.11管理的最大物理内存是16M,对于不同大小的内存的区域划分是不一样的,以16M物理内存来说
mem_map用来管理除了内核外的15M内存
//内存映射字节图(1 字节代表1 页内存),每个页面对应的字节用于标志页面当前被引用(占用)次数。 |
mem_init函数过后把3M缓冲区和虚拟盘所在的mem_map字节置为USED宏表示页面被占用标志,主内存区mem_map置为0
trap_init函数初始化了中断描述符表,中断描述符TYPE位均为1111即为陷阱门,目标代码段描述符选择子均为8(RPL=0,TI=0,索引=1),,其中3-5号中断描述符的dpl设置为3,其余为0
调用INT3 int n INTO等指令产生的中断或异常检测要求
- 先检测当前特权级CPL<=中断描述符里的DPL
- 再检测中断描述符里代码段描述符选择子的RPL < 当前特权级CPL
由硬件产生的中断或处理器检测到的异常检测要求
- 中断描述符里代码段描述符选择子的RPL < 当前特权级CPL
陷阱门和中断门唯一区别就是中断门处理时会把IF位清零(关中断),陷阱门不会
sched_init初始化了任务0在全局描述符表gdt的tss和ldt分别为0x00008901f4480068 0x00008201f4300068
0x00008901f4480068: S=0 TYPE:1001 为tss段描述符
0x00008201f4300068 : S=0 TYPE:0010 为 ldt描述符
tss和ldt描述符的基地址都是该任务task_struct结构体里的成员的地址
struct task_struct |
然后把task数组除了第一个任务都清空,再把其余gdt表项都清空,把eflags寄存器的NT位置零,防止后面在iret到用户态时时进行任务切换,把tr寄存器设置为任务0的tss
对于寻找tss的宏一般人都会这样写#define _TSS2(n) (FIRST_TSS_ENTRY+2*n)<<3
linus的写法#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
感觉对于当时性能可能会高些,但是现在编译器优化的bt程度可能性能上没有差别
ltr val指令会把gdt里索引val tss描述符基地址和段限长加载,并把TYPE的B置1,表示任务正忙
设置ldtr寄存器指向任务0的ldt
设置并允许时钟中断处理,设置系统调用中断0x80(陷阱门,DPL=3)
TODO
利用iret中断返回切换到用户态
init_task的ldt为INIT_TASK宏里ldt[1]=0x00c0fa000000009f ldt[2]=0x00c0f2000000009f
/* ldt[3]*/ {{0, 0}, \ |
堆栈段选择符SS:0x17:00010 1 11 代码段选择符为0xf:1 1 11分别对应ldt里的第二项和第一项
kernel
asm.s
at&t语法差别
xchgl %eax,(%esp) eax和esp栈顶里的值交互 |
对于无错误码的中断压入0作为默认错误码,这样统一起来处理比较简单(保存现场,传递两个参数old_esp就是cpu帮我们压入几个寄存器的eip的位置,和error_code)
trap.s
疑惑的点大多是gcc的“语法”和一些不常见的asm inst
|
首先就是({…})的语句块,语义上这个语句块等于一条语句,语句块的局部变量随语句块结束失效,语句块最后一个表达式就是这个语句块的返回值
int v = x+({puts("gr");z+3;})+y 等价于 v= x+z+3+y |
lsl指令 load segment limit
str指令 store task regist
TODO 暂时没读懂的点: die里str(i)如何获取任务号
system_call.s
syscall的时候fs寄存器保持了三环状态,可以用来访问用户数据
用push和jmp模拟可指定返回地址的call
pushl $ret_from_sys_call |
sys_execve和sys_fork是个系统调用不是中断,用了ret而不是iret,(可能是不想单独再开个asm文件写)
sched.c
**show_task()**函数
要理解j = 4096-sizeof(struct task_struct);需要看下面的联合体 可以看出j就是栈的大小
union task_union |
后面就是(p + 1)也就是跳过这个结构体,去得到未使用的栈大小,因为栈是从高地址往低地址长的
i = 0; |
**schedule()**函数
里首先比较了每个任务的task_struct->alarm和全局的滴答数字jiffies,小于则设置SIGALRM信号,jiffies是从开机到现在的滴答数,10ms一次的时钟中断用sys_call.s里的_timer_interrupt中断处理会把jiffies+1
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && |
然后比较得到任务中最大的时间片task->count,switch_to函数切换到该任务(使用ljmp tss切换的,会把当前寄存器现场写到当前tss段(也就是说switch_to函数在下次该任务调度前不会返回),然后恢复ljmp指定tss的现场执行)
如果时间片都为0则用counter = counter /2 + priority赋值时间片,再次循环
sleep_on函数
void sleep_on (struct task_struct **p)//*p是因为正在等待某个资源而睡眠的任务的队列链表的头部 |
p可以类似于是static struct task_struct *wait_motor[4] = { NULL, NULL, NULL, NULL };
,这个数组就相当于四个链表头
*p和当前任务的栈上的tmp指针就会形成下面等待某一个资源的任务链表
当资源可以被抢占后,上图中的*p指向的”当前任务“返回执行调度前的代码,就可以从当前栈上访问到tmp了
if (tmp) // 若还存在等待的任务,则也将其置为就绪状态(唤醒)。 |
也就是把task2的state置为可调度状态,task2被调度执行时,task2重复“当前任务”上面的过程,把task1的state置为可调度,task1的tmp=0,说明他是第一个等待该资源的任务,没用上一个了
interruptible_sleep_on()函数
repeat: current->state = TASK_INTERRUPTIBLE; |
signal.c
首先是signal.h里的一个难理解的函数声明
void (*signal(int _signr, void (*_handle)(int)))(int); |
写成下面的方式更容易理解
typedef void sigfun(int) |
do_signal()函数
do_signal函数是在system_call.s中ret_from_sys_call检测到current->signal有信号时被调用,do_signal的参数也是在那里压入栈的
do_signal里巧妙处理
*(&eip) = sa_handler;//改掉进入中断时处理器帮助我们压入的eip,这样返回到ret_from_sys_call里iret时就会执行用户态的信号处理函数 |
此时栈的情况
要理解if (!(sa->sa_flags & SA_NOMASK))//没有设置SA_NOMASK即允许信号自己的处理句柄收到信号自己
需要看下面的sys_sigaction函数里的内容
if (current->sigaction[signum - 1].sa_flags & SA_NOMASK) |
do_signal最后一句current->blocked |= sa->sa_mask;设置阻塞当前信号
,这里put_fs_long(current->blocked,tmp_esp++);
保存主要是为了信号处理完后恢复阻塞,从而避免了传统sys_signal的问题
sa_restorer也根据是否设置SA_NOMASK分为下面两种情况
此时设置了SA_NOMASK会调用signal.c的sys_ssetmask (int newmask)恢复
int sys_ssetmask (int newmask) |
再进行add esp的外平栈,再pop,再ret到old_eip
exit.c
TODO do_exit里文件,目录和tty相关的没读懂,
sys_close (i);
iput (current->pwd);
tty_table[current->tty].pgrp = 0;
fork.c
复习下内存管理,每个任务逻辑空间是64M
所有操作系统管理所有任务共用0地址处的页目录表即可
verify_area()函数用来copy on write,在写某个地址addr之前调用,addr可写就直接返回了,否则会复制该页到新物理页
void verify_area (void *addr, int size) |
copy_process() find_empty_process()函数
读之前先看一下fork的系统调用实现
_sys_fork: |
find_empty_process里
repeat: |
_copy_process里
p->tss.esp0 = PAGE_SIZE + (long) p; // 内核堆栈指针(由于是给任务结构p 分配了1 页新内存,所以此时esp0 正好指向该页顶端)。 |
copy_mem函数把nr*64M处线性地址给ldt基址,并把这部分线性基址页表映射到当前进程的物理内存即父进程的物理内存
sys.c
sys_setregid函数
if (egid>0) { |
vsprintf.c
skip_atoi函数
while (is_digit (**s)) |
do_div函数
|
number函数
while (num != 0)//do_div()num写回商 返回余数 |
kernel blk_drv
mm
memory.c
free_page
if (mem_map[addr]--) return; |
int copy_page_tables(unsigned long from,unsigned long to,long size) fork中被调用
//其中from和to都是线性地址 size单位为字节,读的时候要铭记0.11所以进程共用4G的线性地址空间 |
unsigned long put_page(unsigned long page,unsigned long address)
//page是主内存区物理地址,address是线性地址 |
void un_wp_page(unsigned long * table_entry)
table_entry是页表项的物理地址 |
void do_wp_page(unsigned long error_code,unsigned long address)
//address线性地址 |
TODO:do_no_page从磁盘加载缺页的内容
page.s
通过判断page fault error code 的最低位P判断是由缺页还是页写保护引起的缺页异常,决定调用do_no_page还是do_wp_page
fs
buffer.c
remove_from_queues
if (!(bh->b_prev_free) || !(bh->b_next_free)) |
看不懂的话要好好看一下书这里LRU链表得描述
buffer_init
while ( (b = (char*)b - BLOCK_SIZE) >= ((void *) (h+1)) ) { |
头尾相向而行,直到摩擦碰撞,b = (char*)b - BLOCK_SIZE) 是下一个缓冲块得位置,h+1是该缓冲块对应得缓冲头