linux0.00 代码阅读笔记

----------------------------------------
汇编语言的编写, 编译, 运行,调试:
author: hjjdebug
date:

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

参考代码地址:GitHub - hjjdebug/linux0.00: linux0.00 code, compiled under ubuntu14.04, gcc 4.8

ubuntu14 的环境这里用gdb(4.8.4)+bochs(2.4.6)来调试

原来曾经读懂了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 没有小汇编功能, 测试还需要编写,编译,运行这个过程, 所以需要一个简单环境。
如果直接调试,也要很方便的中断,然后跟踪调试。
最后还是选用了带gdb 调试功能的bochs, 它与带dbg调试功能的bochs 相冲突,只能编译出一种功能, 还是带gdb调试接口的胜出.
----------------------------------------
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 的次高位, (d/b位),为1表示采用32位。为0是16位,后面的两位可认为固定为00
0x92:  9 的最高位是存在位, 次2位00 表示描述符特权级, 下一位为1表示为代码或数据,若为0是系统位, 2为段类型
----------------------------------------
3. iret 指令详解: (从优先级0 转入优先级3,并切换到用户栈)
    pushl $0x17
    pushl $usr_stk0
    pushfl
    pushl $0x0f
    pushl $task0
    iret
iret 无疑会执行0x0f:task0, 并使用堆栈0x17:usr_stk0, 初始flag 为堆栈中flag.

它确定了CS,PC,EFLAG, SS,及ESP

但是: 还想知道, 0x0f:task0 的线性地址是多少?
1. 因为是0x0f, bit0,bit1表示优先级为3,

bit2 TI 位为1,所以选择ldt表,此时ldtr 值为0x0028,
调试器中显示:ldtr:0x0028, dh=0x0000e200, dl=0x0c680040, valid=1

0x0028 是由 ltr %ax ,(ax=0x28)加载的.

后面对应的数值是gdt中0x28处的描述符,是如下定义的.
.word 0x0040, ldt0, 0xe200, 0x0    # LDT0 descr 0x28
ldt0 被编译器计算为0x0c68, 它从这里可以拿到ldt表的起始位置.这就是调试器中显示的信息

下边再说说ldt表的内容.
0x0f 屏蔽低3位为0x08, 为ldt表第2项
.quad 0x00c0fa00000003ff    # base = 0x00000
由于0x0f选择子指向地址的base为0,所以0x0f:task0 的线性地址就是task0。


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


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,为段选择子,低16位是地址低16位
    movw $0xef00, %dx                         ;edx 高16为为0, 是地址高16位, 0xef00参考后述
    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

0xef00表示最高位1表示存在位,此两位11,优先级为3,下一bit表示系统, f为中断门,再后面的00认为是保留位。

所以当int 80 发生时,会进入内核态,因为定义的选择子是8,RPL 是0  (内核态),TI 是0(选择gdt表).

同理,发生时钟中断也是如此,运行在内核态.选择的是gdt表.

堆栈及指针(0x10 : 0x11ec)从哪里获得? 

是从TSS0 任务段描述符中获取的,这里定义了
中断时使用了kernel stack: krn_stack  0x1200

运行级别发生改变,就要从这里拿堆栈地址了!

看一下堆栈中保留的内容:
x/10 esp
0x000011ec <bogus+       0>:    0x0000200b    0x0000000f    0x00000246    0x00002214
0x000011fc <bogus+      16>:    0x00000017    0xaaaaaaaa
0x17:2214 : 用户的堆栈指针
0x246    :    用户flag
0xf:200b:    被中断的用户pc指针
这样,一个iret 指令就可以恢复到原来位置了。

定时器中断到后,将会进行任务切换!
----------------------------------------
5. 定时器中断:
在定时器中断服务程序入口设置断点,断不下来,靠!
再下面一条语句,可以断下来. 好! 继续分析...
定时器中断,跟int 80h 中断是很相像的。
此时看, 代码段值为0x08, 堆栈段值为0x10 why?
这要问问8号中断门了.
8号定时器中断:
    movl $0x00080000, %eax    
    movw $timer_interrupt, %ax
    movw $0x8E00, %dx         # edx 高16位为0,是32位基地址的高16位
    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. 优先级3呢? 这就是本任务的ss 和 esp, 保存在tss0后面结构项了.

目前堆栈中都保存了什么东西了呢?
<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
tr 寄存器内容也随之改变为TSS1_SEL,从它的描述符中对应位置取到一大堆信息,恢复环境.
一个跳转, 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.
可见100个任务需要设置100个内核栈,100个用户栈.且互相不可重叠,才不会乱!
----------------------------------------
7. 其它问题:
问:任务被切走, 中断的iret 何时执行?
答:要等任务再被切换回的时候,当切走时,就是切换任务的下一条指令地址,被保留进TSS的EIP寄存器处,恢复时,还从那恢复。

   还恢复了一堆別的寄存器,接着就可以走后面的iret 指令了,

 我也是通过debug 跟踪理解的. 当时一个ljmp 跳走了,还以为走不到 jmp 2f,实际上它还是能回来的.

   只所以难理解,任务切换实际上除了第一次是跳到任务中开始处,其它都是跳转

   到中断程序中任务切换的下一条指令,即ljmp的下一条指令, 那里是后续的接头!!


问: 这样说,用户进程被时钟打断后,中断服务程序要执行很久才能走完了,不是中断服务程序
    执行的越短越好吗?!
答: 这个问题要分开来说,

     从用户的角度看, 程序被中断了,中断服务程序执行了很久(它被切换执行别的任务了),然后才返回来(通过iret返回)。.
    从系统的角度看,程序被中断以后,中断服务程序首先发送EOI 允许后续硬件中断请求,然后进行任务切换,切换是从新任务段中恢复中断标志,中断使能再次打开,可以认为中断服务已经完成.


    这里,我们从两个角度,解释了什么叫中断服务程序,侧重面不同。一个说再次打开中断为服务完成,一个说重回我程序为服务完成.角度不同.但描述的是不同侧面.

    这就是多任务切换的实质 . 单任务中断程序中断了很快返回,多任务被中断程序中断后引起任务切换,等轮到你的时候再返回.
 

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

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

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

小结2:

1. 什么是进程.
  进程是程序调入内存后的一个执行实例.每个进程拥有自己独立的资源,代码段,数据段,堆栈段.
  386可以用一个ldt表来描述,而这个ldt表,用gdt表中ldt描述符来描述.
  进程切换需要保留当前的进程状态,它将保留在一个任务状态段(TSS)中,并从被切换到的任务状态段中获取执行的上下文开始运行.
  任务状态段也用gdt表中一个描述符来描述,叫tss描述

 每个进程需要一个LDT描述符和一个TSS描述符,分别对应TSS表和LDT表.

TSS表用来保存现场.

LDT表用来定义代码段,数据段,堆栈段等.因为X86采用的是段式寻址方式.段+偏移方式


    考察这两个简单的进程,task0,task1,它们真是不能再简单了.
    需要有两个任务状态段tss0,tss1,用来保存任务切换时的寄存器内容, 这样在gdt表中需要加2个描述符
    每一个任务需要一个ldt描述符, 每一个ldt描述符对应一个ldt表,包含定义代码段,数据段描述符等,描述符的偏移就确定了 cs,ds,ss就是所谓的段式寻址,所以段式寻址编程先要设一堆表,而编译器其实只认别偏移地址

 

2. 任务状态段存在的必要性:
    任务状态段存在的必要性是因为intel cpu运行于内核态与运行于用户态都共用一套寄存器, 在任务切换时,需要保留它们,
    为了简化操作,开辟一块内存,用硬件一次性把寄存器内容存入其中,省去了用户在堆栈中保留一大堆寄存器的过程.
    这样用一条指令即能完成任务切换. ljump TSS0_SEL 或ljump TSS1_SEL, 长跳转到任务状态段1描述符或任务状态段2描述符
    这样自动完成ldt切换,恢复cs,ds,ss等段寄存器及常规寄存器,堆栈寄存器及其eip(代码执行位置).
    据说arm 内核态与用户态用两套寄存器,则没有了级别变化时保存寄存器这一拖累!不过任务切换还是要保留寄存器.

3. 第一次进入用户态运行,用的是iret指令

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值