# head.s包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码。
# 在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和1之间的切换操作。
LATCH       = 11930                # 定时器初始计数值,即每隔10毫秒发送一次中断请求。  问:为何是这个值?
SCRN_SEL    = 0x18             # 屏幕显示内存段选择符。   问:以下这些选择符是怎么定的值?
TSS0_SEL    = 0x20             # 任务0的TSS段选择符。
LDT0_SEL    = 0x28             # 任务0的LDT段选择符。
TSS1_SEL    = 0x30             # 任务1的TSS段选择符。
LDT1_SEL    = 0x38             # 任务1的LDT段选择符。
.global startup_32  # 作用?
.text   # 表示可执行代码段(问:实际在编译时有什么影响吗?)
startup_32:
# 首先加载数据段寄存器DS、堆栈段寄存器SS和堆栈指针ESP。所有段的线性基地址都是0.
movl    $0x10, %eax     # 0x10是GDT中数据段选择符。
# 解释一下以上的数据段选择符为什么是0x10:
# 首先,要知道段选择符的格式为:15-3:描述符索引;2:TI(Table index表指示标志);1-0:RPL(Requested Privilege Level请求特权级)
# 这里,实际上数据段应该是第二个段,故索引的二进制为10,而TI和RPL都是0,所以,后面添三位0,乘以8,故选择符即为:0x10(=10 0 00)
mov     %ax, %ds        # ds就是存放数据段选择符的段寄存器,这条指令将ax的值0x0010传递给ds寄存器
lss     init_stack, %esp    # LSS:加载堆栈段(问:标号init_stack默认是一个多少位的地址?)
# lss mem, reg: mem低字->reg,mem高字->ss (问:1.低字和高字分别占多少位?这样说来,该堆栈段的长度即使2^(mem低字的位数喽?) 2.指令有什么影响? 3.堆栈段描述符为什么不在GDT中?)
# 在新的位置重新设置IDT和GDT表。
call    setup_idt
call    setup_gdt
movl    $0x10, %eax
mov %ax, %ds        # ds没有变,这句可省略
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss init_stack, %esp    # ss和esp也都没有变,这句可省略
# 设置8253定时芯片。把计数器通道0设置成每隔10毫秒向中断控制器发送一个中断请求信号。
# 下面介绍一下8253定时芯片:
# 8253具有3个独立的计数通道,采用减1计数方式。在门控信号有效时,每输入1个计数脉冲,通道作1次计数操作。当计数脉冲是已知周期的时钟信号时,计数就成为定时。
# 方式3为:方波发生器,最适合计算机。
movb    $0x36, %al              # 控制字:设置通道0工作在方式3、计数器初值采用二进制。
movl    $0x43, %edx             # 8253芯片控制字寄存器写端口。
outb    %al, %dx
movl    $LATCH, %eax            # 初始计数值设置为LATCH(1193180/100),即频率100HZ。(问:这里是什么意思?1193180是怎么出来的?)
movl    $0x40, %edx             # 通道0的端口。
outb    %al, %dx                # 分两次把初始计数值写入通道0.
movb    %ah, %al
outb    %al, %dx
# 在IDT表第8和第128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符。
# 这里先解释一下int $0x80:
# int $0x80是一条AT&T语法的中断指令,用于Linux的系统调用。
# Linux系统下的汇编语言比较喜欢用AT&T的语法,如果翻译成Intel的语法就是int 80h,就像我们在Intel的语法下的DOS汇编中经常用的int 21h调用DOS中断,同样如果换成AT&T语法就是int $0x80。
# 不过无论使用那一种语法,int $0x80或者int 80h都是针对Linux的,在DOS或者Windows下不起相应作用。反之亦然。
movl    $0x00080000, %eax       # 中断程序属内核,即EAX高字是内核代码段选择符0x0008(即索引为1,TI=0,RPL=00)
movw    $timer_interrupt, %ax   # 设置定时中断门描述符。取定时中断处理程序地址。
movw    $0x8E00, %dx            # 中断门类型是14(屏蔽中断),特权级0或硬件使用。
movl    $0x08, %ecx             # 开机时BIOS设置的时钟中断向量号8.这里直接使用它。
lea     idt(, %ecx, 8), %esi    # 把IDT描述符0x08地址放入ESI中,然后设置该描述符
movl    %eax, (%esi)
movl    %edx, 4(%esi)
movw    $system_interrupt, %ax  # 设置系统调用陷阱门描述符。取系统通调用处理程序地址。
movw    $0xef00, %dx            # 陷阱门类型是15,特权级3的程序可执行。
movl    $0x80, %ecx             # 系统调用向量号是0x80。
lea     idt(, %ecx, 8), %esi    # 把IDT描述符项0x80地址放入ESI中,然后设置该描述符。
movl    %eax, (%esi)
movl    %edx, 4(%esi)
# 好了,现在我们为移动到任务0(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景。
# 注: 由于处于特权级0的代码不能直接把控制权转移到特权级3的代码中执行,但中断返回操作是可以的,因此当初始化GDT、IDT和定时芯片结束后,我们就利用中断返回指令IRET来启动运行第1个任务。
#     具体实现方法是在初始堆栈init_stack中人工设置一个返回环境。即把任务0的TSS段选择符加载到任务寄存器LTR中、LDT段选择符加载到LDTR中以后,
#     把任务0的用户栈指针(0x17:init_stack)和代码指针(0x0f:task0)以及标志寄存器压入栈中,然后执行中断返回指令IRET。
#     该指令会弹出堆栈上的堆栈指针作为任务0的用户栈指针,恢复假设的任务0的标志寄存器内容,并且弹出栈中代码指针放入CS:EIP寄存器中,从而开始执行任务0的代码,
#     完成了从特权级0到特权级3的控制转移。
pushfl                          # 复位标志寄存器EFLAGS中的嵌套任务标志。
andl    $0xffffbfff, (%esp)
# 解释一下EFLAGS寄存器中的NT标志:
# 位14是嵌套任务标志(Nested Task)。它控制这被中断任务和调用任务之间的链接关系。在使用CALL指令、中断或异常执行任务调用时,处理器会设置该标志。在通过使用IRET指令从一个任务返回时,处理器会检查并修改这个NT标志。
# 使用POPF/POPFD指令也可以修改这个标志,但是在应用程序中改变这个标志的状态会产生不可意料的异常。
# 嵌套任务标志NT用来控制中断返回指令IRET的执行。具体规定如下:
# (1) 当NT=0,用堆栈中保存的值恢复EFLAGS、CS和EIP,执行常规的中断返回操作;
# (2) 当NT=1,通过任务转换实现中断返回。
popfl
movl    $TSS0_SEL, %eax         # 把任务0的TSS段选择符加载到任务寄存器TR。
ltr     %ax
movl    $LDT0_SEL, %eax         # 把任务0的LDT段选择符加载到局部描述符表寄存器LDTR。
lldt    %ax                     # TR和LDTR只需人工加载一次,以后CPU会自动处理。
movl    $0, current             # 把当前任务号0保存在current变量中。
sti                             # 现在开启中断,并在栈中营造中断返回时的场景。
pushl   $0x17                   # 把任务0当前局部空间数据段(堆栈段)选择符如栈。
# 问:0x17是怎么来的?
# 答:0x17是任务0的数据段选择符,由下面设置的ldt0可知,数据段Index=2,TI=1(表示在LDT中),RPL=3(处理器的保护机制可识别4个特权级,0级到3级,详见4.5.1 段级保护),故得0x17
pushl   $init_stack             # 把堆栈指针入栈(也可以直接把ESP入栈)。
pushfl                  # 把标志寄存器入栈。
pushl   $0x0f               # 把当前局部空间代码段选择符入栈。
pushl   $task0                  # 把代码指针入栈。注意!pushl和push也是有区别的,我之前写成了push,运行就出错了!
iret                            # 执行中断返回指令,从而切换到特权级3的任务0中执行。
# 以下是设置GDT和IDT中描述符项的子程序。
setup_gdt:                      # 使用6字节操作数lgdt_opcode设置GDT表位置和长度。
lgdt    lgdt_opcode     # lgdt指令加载GDT的入口地址(这里由lgdt_opcode指出)到GDTR中
ret
setup_idt:
lea     ignore_int, %edx    # 设置方法与设置定时中断门描述符的方法一样。
movl    $0x00080000, %eax   # 选择符位0x0008。
movw    %dx, %ax            # (注:ax为eax的低16位)
movw    $0x8E00, %dx
lea     idt, %edi
mov     $256, %ecx          # 循环设置所有256个门描述符项。
rp_idt: movl    %eax, (%edi)
movl    %edx, 4(%edi)
addl    $8, %edi
dec     %ecx
jne     rp_idt
lidt    lidt_opcode
ret
# 显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上。整屏可显示80X25个字符。
write_char:
push %gs
pushl %ebx
#   pushl %eax
mov $SCRN_SEL, %ebx
mov %bx, %gs
movl scr_loc, %ebx
shl $1, %ebx
movb %al, %gs:(%ebx)
shr $1, %ebx
incl %ebx
cmpl $2000, %ebx
jb 1f
movl $0, %ebx
1:  movl %ebx, scr_loc
popl %ebx
pop %gs
ret
# 以下是3个中断处理程序:默认中断、定时中断和系统调用中断。
# ignore_int是默认的中断处理程序,若系统产生了其他中断,则会载屏幕显示一个字符‘C’。
.align  2   # align是对齐的指令   (注意:之后来好好研究一下关于对齐这个问题)
ignore_int:
push    %ds
pushl   %eax
movl    $0x10, %eax     # 首先让DS指向内核数据段,因为中断程序属于内核。
mov     %ax, %ds
movl    $67, %eax       # 在AL中存放字符'C'的代码,调用显示程序显示在屏幕上。
call    write_char
popl    %eax
pop     %ds
iret
# 这是定时中断处理程序。其中主要执行任务切换操作。
.align  2
timer_interrupt:
push    %ds
pushl   %eax
movl    $0x10, %eax     # 首先让DS指向内核数据段。这两句不要不影响。
mov     %ax, %ds
movb    $0x20, %al      # 然后立刻允许其他硬件中断,则向8253发送EOI命令。 这两句必须要!
outb    %al, $0x20
movl    $1, %eax
cmpl    %eax, current
je 1f
movl    %eax, current   # 若当前任务是0,则把1存入current,并跳转到任务1
ljmp    $TSS1_SEL, $0   # 去执行。注意跳转的偏移值无用,但需要写上。
jmp     2f
1:  movl    $0, current     # 若当前任务是1,则把0存入current,并跳转到任务0
ljmp    $TSS0_SEL, $0
2:  popl    %eax
pop     %ds
iret
# 系统调用中断int0x80处理程序。该示例只有一个显示字符功能。
# 说明:system_interrup这个中断处理程序将由两个任务来调用。
.align  2
system_interrupt:
push    %ds
pushl   %edx
push    %ecx
pushl   %ebx
pushl   %eax
movl    $0x10, %edx # 首先让DS指向内核数据段
mov %dx, %ds
call    write_char  # 然后调用显示字符子程序write_char, 显示AL中的字符
popl    %eax
popl    %ebx
popl    %ecx
popl    %edx
pop %ds
iret
/*****************************************************************/
current:    .long 0         # 当前任务号(0或1)。
scr_loc:    .long 0         # 屏幕显示位置。按从左上角到右下角顺序显示。
.align  2
lidt_opcode:
.word   256*8-1         # 加载IDTR寄存器的6字节操作数:表长度和基地址。
.long   idt
lgdt_opcode:
.word   (end_gdt-gdt)-1 # 这个16位数表示GDT的段限长(注意:书P88:限长为0表示有1个有效字节。因为段描述符总是8字节长,因此GDT的限长值应该设置成总是8的倍数减1(即8N-1))   问:1.N在哪里?
.long   gdt             # 这个32位数表示GDT的基地址
.align  8
idt:    .fill   256,8,0         # IDT表空间。每个门描述符8字节,共占用2KB字节。(注:.fill伪指令???)
gdt:    .quad   0x0000000000000000      # GDT表。第1个描述符不用。
.quad   0x00c09a00000007ff      # 第2个是内核代码段描述符。其选择符是0x08。
.quad   0x00c09200000007ff      # 第3个是内核数据段描述符。其选择符是0x10。
.quad   0x00c0920b80000002      # 第4个是显示内存段描述符。其选择符是0x18。
.word   0x68, tss0, 0xe900, 0x0 # 第5个是TSS0段的描述符。其选择符是0x20
.word   0x40, ldt0, 0xe200, 0x0 # 第6个是LDT0段的描述符。其选择符是0x28
.word   0x68, tss1, 0xe900, 0x0 # 第7个是TSS1段的描述符。其选择符是0x30
.word   0x40, ldt1, 0xe200, 0x0 # 第8个是LDT1段的描述符。其选择符是0x38
end_gdt:
.fill   128,4,0                 # 初始内核堆栈空间(问:.fill是什么意思?)
init_stack:
.long   init_stack              # 堆栈段偏移位置。
.word   0x10                    # 堆栈段同内核数据段
# 下面是任务0的LDT表段中的局部段描述符。
.align 8
ldt0:   .quad   0x0000000000000000      # 第1个描述符,不用。
.quad   0x00c0fa00000003ff      # 第2个局部代码段描述符,对应选择符是0x0f
.quad   0x00c0f200000003ff      # 第3个局部数据段描述符,对应选择符是0x17
# 下面是任务0的TSS段的内容。注意其中标号等字段在任务切换时不会改变。
tss0:   .long   0                       /* back link */
.long   krn_stk0, 0x10          /* esp0, ss0 */
.long   0, 0, 0, 0, 0           /* esp1, ss1, esp2, ss2, cr3 */
.long   0, 0, 0, 0, 0           /* eip, eflags, eax, ecx, edx */
.long   0, 0, 0, 0, 0           /* ebx, esp, ebp, esi, edi */
.long   0, 0, 0, 0, 0, 0        /* es, cs, ss, ds, fs, gs */
.long   LDT0_SEL, 0x8000000     /* ldt, trace bitmap */
.fill   128, 4, 0               # 这是任务0的内核栈空间。
krn_stk0:
# 下面是任务1的LDT表段内容和TSS段内容
.align 8
ldt1:   .quad   0x0000000000000000      # 第1个描述符,不用
.quad   0x00c0fa00000003ff      # 选择符是0x0f,基地址=0x00000。
.quad   0x00c0f200000003ff      # 选择符是0x17,基地址=0x00000。
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 */
.fill   128, 4, 0               # 这是任务1的内核栈空间。其用户栈直接使用初始栈空间。
krn_stk1:
# 下面是任务0和任务1的程序,他们分别循环显示字符'A'和'B'。
task0:
movl    $0x17, %eax         # 首先让DS指向任务的局部数据段。
movw    %ax, %ds            # 因为任务没有使用局部数据,所以这两句可省略。
movb    $65, %al            # 把需要显示的字符'A'放入寄存器中。
int $0x80               # 执行系统调用,显示字符。
movl    $0xfff, %ecx            # 执行循环,起延时作用。
1:  loop    1b
jmp task0               # 跳转到任务代码开始处继续显示字符。
task1:
movb    $66, %al            # 把需要显示的字符'B'放入寄存器中。
int $0x80               # 执行系统调用,显示字符。
movl    $0xfff, %ecx            # 执行循环,起延时作用。
1:  loop    1b
jmp task1               # 跳转到任务代码开始处继续显示字符。
.fill   128,4,0             # 这是任务1的用户栈空间。
usr_stk1:

Linux 0.00简单多任务内核head.s超详注释相关推荐

  1. Linux 0.00简单多任务内核boot.s超详注释

    ;标题:简单内核引导启动程序 ;作者:黄旭冬 ;程序描述: ; 该程序首先跳到0x07c00处,这是BIOS开始时将本程序加载到的位置, ; 然后本程序将内核代码(head.s代码)加载到0x1000 ...

  2. Linux 0.00 代码解析(一)

    <Linux内核完全剖析>这本书在第4章给出了一个简单多任务内核示例程序,作者称之为Linux 0.00系统. 源码的下载地址和实验方法可以参考我的博文 http://blog.csdn. ...

  3. Linux 0.00 的编译和运行

    <Linux内核完全剖析>这本书在第4章给出了一个简单多任务内核示例程序,我们称之为Linux 0.00系统.本文介绍了一种把它跑起来的方法. 一.实验环境 Win7(64位)+Vmwar ...

  4. Linux 0.00 Makefile 说明

    关于Linux-0.00 的编译和运行可以参考我的博文:http://blog.csdn.net/longintchar/article/details/78757065 为了能在64位的Ubuntu ...

  5. 4.9一个简单的多任务内核实例

    第四章第9节 本节描述了一个简单多任务内核的设计和实现方法,这个内核包括两个特权级3的用户任务和一个系统调用中断过程. 本节给出的内核实例由两个文件构成.一个是使用as86语言编制的引导启动程序boo ...

  6. bochs调试linux内核学习4 - bochs配置文件的$BXSHARE变量、bochs的System BIOS must end at 0xfffff错误、运行内核0.00版本

    经过前面的一些操作,目前只能用bochs调试linux 0.11内核的启动部分:下面来继续学习相关内容: 根据资料,在内核0.11之前,还有更小的内核0.00版本需要认识:在此下载: http://o ...

  7. Linux 0.11内核分析03:系统调用

    目录 1 概述 1.1 什么是系统调用 1.2 为什么需要系统调用 2 系统调用基础设施 2.1 安装系统门 2.1.1 中断描述符 2.1.2 中断描述符安装函数 2.1.3 安装0x80系统门 2 ...

  8. Linux 0.11内核分析02:系统启动

    目录 1. 内核镜像的构建 1.1 内核源码结构 1.1.1 boot 1.1.2 fs 1.1.3 include 1.1.4 init 1.1.5 kernel 1.1.6 lib 1.1.7 m ...

  9. linux 0.11 内核学习 -- bootsect.s, 万里长征第一步

    呵呵,终于将linux 0.11 下面的boot文件夹下的三个文件读完,下面是相关注释,没有汇编基础的人也是可以读的.废话少说,下面就是linux的源码了. 参考资料 Linux内核完全注释.pdf ...

最新文章

  1. Android Q:新系统名称和新特性整理
  2. Spring源码系列:BeanDefinition源码解析
  3. Numpy-创建数组
  4. access exex控制pc_ownCloud/Nextcloud文件访问控制(Files Access Control)
  5. Facebook揭秘其应用测试平台,并开源核心技术
  6. 外网访问FTP服务,解决只能以POST模式访问Filezilla的问题
  7. 文本框获取和失去焦点默认值问题
  8. 谁说 Java 不能用来跑 Serverless?
  9. EntityFramework中IEnumerable和IQueryable的含义和区别
  10. python 调用gpu算力_教你免费使用百度云GPU算力提交深度学习任务
  11. wireshark基本用法及过虑规则
  12. python后端数据发送到前端_python后端开发使用flask接收前端数据,处理后返回结果...
  13. .pem和.pk8是什么文件?(转载)
  14. CloudSim源码分析之DatacenterBroker--processEvent()
  15. 数据结构之SWUSTOJ1038: 顺序表中重复数据的删除
  16. 咖啡技能培训 | 成为咖啡师需要注意哪些方面?
  17. 计算机网络 latency,网络中delay和latency的区别
  18. mongo-java 实现使用and拼接多个or查询条件
  19. OSG三维渲染引擎编程学习之五:“第一章:OSG介绍” 之 “1.5 OSG模块”
  20. 对于二维数组,如何用Arrays.sort()进行排序以及理解------通俗易懂,条理清晰

热门文章

  1. 大模型时代,视觉推理任务竟然只用语言数据也能学习
  2. 2021高博会扩大举办,助力高尔夫运动新发展
  3. Microsoft Visual Studio 注册码
  4. 耗时两周的P2PQQ终于完成了!!!
  5. 章节十四:Scrapy框架
  6. [Oracle]-[recyclebin][索引]-回收站恢复的索引名称修改
  7. GLSL着色器实现多重纹理与帧缓冲对象(FBO)
  8. 怎么不用U盘PE制作双系统
  9. Excel-VBA应用(5):设计问卷及数据回收统计系统
  10. 如何解决1万个并发连接,用每个客户一个线程的方法