linux0.00 代码阅读笔记

时间:2022-09-01 08:28:36
----------------------------------------
汇编语言的编写, 编译, 运行,调试:
author: hjjdebug
date:

----------------------------------------

原来曾经读懂了linux0.00代码, 后来翻阅, 发现又有生疏感,干脆写一篇笔记。记录一下。

汇编语言依赖于cpu, 这里以intel 系列(80x86)为研究对象.
1. 汇编语言的书写有它自己的格式,请参考相关书籍。
2. 编译,生成目标文件,可执行文件及列表文件。
     目标文件,可执行文件也可反编译成.s 文件
    objdump86 可以研究8086二进制代码
3. 运行与调试,跟踪程序的执行过程。调试能够观察寄存器,
    观察内存,也能够反编译代码。

bochs. 是intel x86虚拟机。虚拟机好处多,高度可配置,而且
    它不仅可以调试8086 16bits cpu
    而且还可以调试80386 32bits保护模式程序
    运行方式 bochs -f config.bxrc

实例: linux0.00 程序

1. 编写:代码已经书写好了。
2. 编译: 需要先了解Makefile 文件,了解它的编译过程, 还要满足
        自己的一些需求,例如生成列表,反汇编等
    看着列表完备的信息,及寥寥几行boot 8086 代码,不用调试也差不多都懂了。
    顺便说一句, 此时读取磁盘数据只能通过磁头,磁道,扇区调用int13中断读取。

3. 调试:
    bochs 调试由于没有初始化文件, 每次调试做重复性劳动太过浪费时间
    把设置断点以及其它设置(例如每条指令执行前显示寄存器等)放到一个键盘序列上。
    熟悉它的调试功能, h 有帮助信息, s 单步, n 宏单步, r寄存器,m内存等
    调试一定要顺手,熟悉功能,满足自己需要才能有效率。


基本技能: 验证简单汇编代码(80386保护模式)功能, 步骤!
由于bochs-dbg 没有小汇编功能, 测试还需要编写,编译,运行这个过程, 所以需要一个简单环境。
如果直接调试,也要很方便的中断,然后跟踪调试。(选用此法)

----------------------------------------
linux0.00 关键代码分析:
----------------------------------------
1. 加载堆栈段及堆栈指针

addr    code                  ;comment
0007 lss init_stack,%esp      ;0FB225 600C0000
......

0A60 .fill 128,4,0            ; 000000... 128*4 个0
0c60 init_stack:                         
        .long init_stack     ; 600C0000, 定义堆栈地址
        .word 0x10            ; 1000
加载堆栈段寄存器和堆栈指针寄存器。直接寻址指令。
从init_stack 地址所指单元中,加载6字节内容到esp,ss寄存器
这里注意: init_stack 向下,是定义了堆栈位置,
init_stack 向上, 就是堆栈位置。

at&t 汇编:直接寻址是不加()的,寄存器间接寻址是加()的., 立即数加$修饰
    

----------------------------------------
2. 清空屏幕:
把0720数值向显示缓冲区存储单元中丢,就能擦除一个字符,
07是属性,20是空格的ascii
感觉还是很神奇!

显示缓冲区段描述符: .quad 0x00c0920b80000002
换一种写法:         .word 0x0002, 0x8000, 0x920b, 0x00c0

段限:0x0002 加上0x00C0 的最后一个nibble(共20bits) 为段限长2
粒度为页(4K)--C 的最高位, 故段限为8K.
基址: 0x8000 加上0x920b的0b, 加上0x00c0 的00 构成熟知的0xb8000地址
其它:  
C 的次高位, 不知道什么意思。
0x92:  9 的最高位是存在位, 次2位00 表示描述符特权级, 其它5位为类型及属性(可读,可写)
----------------------------------------
3. iret 指令详解: (从优先级0 转入优先级3,并切换到用户栈)
    pushl $0x17
    pushl $usr_stk0
    pushfl
    pushl $0x0f
    pushl $task0
    iret
iret 无疑会执行0x0f:task0, 并使用堆栈0x17:usr_stk0, 初始flag 为堆栈中flag.

但是: 还想知道, 0x0f:task0 的物理地址是多少?
1. 因为是0x0f, TI 位为1,所以选择ldt表,此时ldtr 值为0x0028,
调试器中显示:ldtr:0x0028, dh=0x0000e200, dl=0x0c680040, valid=1
与代码中定义的一致。
.word 0x0040, ldt0, 0xe200, 0x0    # LDT0 descr 0x28
可见,ldt 表在0x0c68处.
0x0f 屏蔽低3位为0x08, 为ldt表第2项
.quad 0x00c0fa00000003ff    # 0x0f, base = 0x00000
由于0x0f选择子base为0,所以0x0f:task0 的物理地址就是task0。
此时task0的地址为0x2000

同理: 堆栈的地址:
0x17 屏蔽低3为为0x10, 为ldt表第3项
.quad 0x00c0f200000003ff    # 0x17
base 也是0, 堆栈的物理地址就是usr_stk0.
此时usr_stk0地址为0x2214

iret 后,系统会进入用户模式task0, 并在这里运行一段时间,不断调用int 80 软中断(也叫自陷)
在屏幕上显示'A'字符。直到发生定时器中断。
----------------------------------------
4. int 80 中断:
esp: 0x00002214 8724
000f:00002009 (unk. ctxt): int $0x80                 ; cd80

esp: 0x000011ec 4588
[0x00000000000001de] 0008:000001de (unk. ctxt): pushl %ds                 ; 1e
这个跳跃是如何实现的?堆栈是如何切换的?
看80号中断门:
    movw $system_interrupt, %ax        ; eax 高16位是 0x0008
    movw $0xef00, %dx
    movl $0x80, %ecx                ; 中断号0x80
    lea idt(,%ecx,8), %esi
    movl %eax,(%esi)
    movl %edx,4(%esi)

低16为(system_interrupt)为偏移值,高16位为0 (edx 高16位)组合成32位偏移地址
0x0008 为选择子, 解释清了代码段值为8,偏移为system_interrupt

堆栈及指针(0x10 : 0x11ec)从哪里获得? 
中断时使用了kernel stack: krn_stack  0x1200
这个krn_stack 是在TSS0 任务段描述符中定义了的。
看一下堆栈中保留的内容:
x/10 esp
0x000011ec <bogus+       0>:    0x0000200b    0x0000000f    0x00000246    0x00002214
0x000011fc <bogus+      16>:    0x00000017    0xaaaaaaaa
0x17:2214 : 用户的堆栈指针
0x246    :    用户flag
0xf:200b:    被中断的用户pc指针
这样,一个iret 指令就可以恢复到原来位置了。

至于write_char 函数,发现有时候
    movb %al, %gs:(%ebx)  直接连字带颜色都显示了,
而下面这个属性设置改不了, 就不理它了。
    movb $0x7, %gs:1(%ebx)

定时器中断到后,将会进行任务切换!
----------------------------------------
5. 定时器中断:
在定时器中断服务程序入口设置断点,断不下来,靠!
再下面一条语句,可以断下来. 好! 继续分析...
定时器中断,跟int 80h 中断是很相像的。
此时看, 代码段值为0x08, 堆栈段值为0x10 why?
这要问问8号中断门了.
8号定时器中断:
    movl $0x00080000, %eax    
    movw $timer_interrupt, %ax
    movw $0x8E00, %dx
    movl $0x08, %ecx              # The PC default timer int.
    lea idt(,%ecx,8), %esi
    movl %eax,(%esi)
    movl %edx,4(%esi)
低16为(timer_interrupt)为偏移值,高16位为0 (edx 高16位)组合成32位偏移地址
0x0008 为选择子, 解释清了代码段值为8,偏移为timer_interrupt

堆栈及指针(0x10 : 0x11e4)从哪里获得?
首先,中断时使用了kernel stack: krn_stack  0x1200
原来是在TSS0 任务段描述符中定义过,  为恢复现场提供数据
tss0:    .long 0             /* back link */
    .long krn_stk0, 0x10        /* esp0, ss0 */
    .long 0, 0, 0, 0, 0        /* esp1, ss1, esp2, ss2, cr3 */
优先级1,2没有对应代码,所有初始化为0.

目前都保存了什么东西了呢?
<bochs:152> x/10 esp
0x000011e4 <bogus+       0>:    0x00000041    0x00000017    0x00002010    0x0000000f
0x000011f4 <bogus+      16>:    0x00000246    0x00002214    0x00000017    0xaaaaaaaa

尾巴上0xaaaaaaaa 是一个标记(0x1200地址),以此分界,下为堆栈
0x17:0x2214 保存的是用户进程堆栈位置
0x246 为用户进程flag
0x0f:2010: 保存用户进程执行代码位置。
此时的eflag, 已经是0x46, 中断标志if 已经清除,不能再接受可屏蔽中断。
0x17: 中断代码刚刚保存的ds 寄存器
0x41: 中断代码刚刚保存的eax 寄存器
后两个值已经是用户行为了。
----------------------------------------
6. 任务切换: 长跳转指令
    ljmp $TSS1_SEL, $0
从中断程序中,长跳转到一个任务段选择子。
当然,要跳转到这个任务段中所保留的位置了.
: ljmp 0030:00000000        ; ea000000003000

一个跳转, IF 被开启
[0x0000000000002218] 000f:00002218 (unk. ctxt): movl $0x00000017, %eax    ; b817000000
看代码:

TSS1_SEL    = 0X30

gdt 表中的对应项是一个任务门描述符。

.word 0x0068, tss1, 0xe900, 0x0    # TSS1 descr 0x30

在 0x1220 地址处
tss1:    .long 0             /* back link */
    .long krn_stk1, 0x10        /* esp0, ss0 */
    .long 0, 0, 0, 0, 0        /* esp1, ss1, esp2, ss2, cr3 */
    .long task1, 0x200        /* eip, eflags */
    .long 0, 0, 0, 0        /* eax, ecx, edx, ebx */
    .long usr_stk1, 0, 0, 0        /* esp, ebp, esi, edi */
    .long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
    .long LDT1_SEL, 0x8000000    /* ldt, trace bitmap */

x/10 0x1220, 观察, 发现正是 0xf:task1 = 0xf:0x2218, 而且 0x200 开启了IF, 使得运行任务1时中断便打开了。
而且, 任务切换,还会用tss0保留了现场。 以供切回tss0 时使用,这些都是硬件直接完成的!
从此,任务要在tss1 下运行一个时间片。 此时用户使用的堆栈是usr_stk1.

----------------------------------------
7. 其它问题:
问:任务被切走, 中断的iret 何时执行?
答:要等任务再被且回的时候,从断掉的地方,就是切换任务的下一条指令执行,恢复了现场
  从而执行到iret, 这个iret 从内核态堆栈中弹出保存地址,正好就是用户中断push进来的。

问: 这样说,用户进程被时钟打断后,中断服务程序要执行很久才能走完了,不是中断服务程序
    执行的越短越好吗?!
答: 这个问题要分开来说, 从用户的角度看, 程序被中断了,中断服务程序执行了很久,才返回来。
    从系统的角度看,程序被中断以后,中断服务程序很快进行到任务切换,同时打开了中断,可以响应
    后续的中断,中断服务程序已经在此时就结束了。后面的时间就是在运行另一个用户程序了。
    这里,从系统角度看,能够继续接受中断为中断服务结束,iret 不再是中断服务程序结束的标记,
    跟常规理解不同,所以有点别扭。
    从用户角度看,仍然可以把iret 作为中断服务结束标记。

小结: 代码运行时使用了2个栈, 用户态运行时发生中断,中断服务程序使用了tss中定义的内核栈。

    时钟中断以及系统调用int80中断都使用这个内核栈。因为它们RPL都是0级(08的低2位)

    任务切换时,保留当前环境到当前tss, 从切换到的tss中恢复现场开始运行, 所有寄存器,段寄存器,
    常规寄存器,cr3, ldt 都被恢复。