调试分析Linux 0.00引导程序
head.s 的工作原理
首先,header.s在32位的保护模式下运行。
- 设置GDT和IDT表
首先加载数据段寄存器、堆栈段寄存器SS和堆栈指针ESP。
movl $0x10,%eax
mov %ax,%ds
lss init_stack,%esp
然后在新的位置重新设置IDT和GDT表。设置IDT,并将256个中断门都填默认处理过程的描述符;设置GDT,在改变了GDT之后重新加载所有段寄存器
call setup_idt
call setup_gdt
movl $0x10,%eax
- 设置系统定时芯片
把计数器通道0设置成每隔10毫秒向中断控制器发送一个中断请求信号。
movb $0x36,%al # 设置计数初值采用2进制
movl $0x43,%edx
outb %al,%dx
movl $LATCH,%eax # 初始计数器的值设置为LATCH
movl $0x40,%edx
outb %al,%dx # 分两次把初始计数器写入通道0
movb %ah,%al
outb %al,%dx
- 在IDT表中8和128表项的位置分别设置中断门描述符和系统调用陷阱门描述符。
中断程序属于内核,也就是说eax的高字节是内核代码段的选择符号 0x0008
设置中断门描述符之后,取定时中断处理程序地址。
设置系统调用陷阱门描述符。取系统调用处理程序地址。
- 移动到任务0来操作堆栈并建立返回场景
复位eflags寄存器中的嵌套任务标志
把任务0的TSS段选择符号加载到任务寄存器TR
把任务0的LDT段选择符号加载到LDTR
存储当前任务号在current变量中
堆栈指针入栈;标志寄存器值入栈;局部空间代码段入栈;代码指针入栈;执行中断返回
另外从LDT和GDT的函数代码可知:
setup_gdt
使用6字节操作数设置GDT表位置和长度,暂时设置IDT表中多有256个终端们描述符都是一个默认值,都是用默认的中断处理过程 ignore_int。
setup_idt
和设置定时终端门描述符的方法一样。选择符为0x0008
timer_interrupt
先让DS指向内核的数据段,然后立刻允许其他的硬件中断,接着判断当前任务,如果是任务1就去执行任务0,反之亦然。
je 1f
movl %eax, current # 如果当前的任务是0,就把1存入current,并转到任务1
ljmp $TSS1_SEL, $0 # 去执行,虽然没有偏移值但是需要写
jmp 2f
movl $0,current # 如果当前任务是1,就把0存入current,并转到任务0
ljmp $TSS0_SEL,$0 # 执行
ignore_int
默认的中断处理程序。每个任务在执行的时候会首先把一个字符的ASCII码值放入寄存器AL中,然后调用系统默认的中断处理程序0x80,显示过字符之后任务代码就会使用循环语句延迟一段时间,然后跳转到另一个任务继续循环执行。任务A显示A,任务B显示B,如果系统产生了其他的中断会显示为C
head.s 内存分布状况
代码段的分布状况
代码段编号 | 名称 | 起始地址 | 终止地址 |
---|---|---|---|
1 | startup_32 | 0x00 | 0xac |
2 | setup_gdt | 0xad | 0xb4 |
3 | setup_idt | 0xb5 | 0xcd |
4 | rp_idt | 0xd2 | 0xe4 |
5 | write_char | 0xe5 | 0x113 |
6 | ignore_int | 0x114 | 0x129 |
7 | timer_interrupt | 0x12a | 0x165 |
8 | system_interrupt | 0x166 | 0x17c |
9 | task0 | 0x10e0 | 0x10f3 |
10 | task1 | 0x10f4 | 0x1107 |
数据段的分布状况
数据段编号 | 名称 | 起始地址 | 终止地址 |
---|---|---|---|
1 | current | 0x17d | 0x180 |
2 | scr_loc | 0x181 | 0x184 |
3 | lidt_opcode | 0x186 | 0x18b |
4 | lgdt_opcode | 0x18c | 0x191 |
5 | idt | 0x198 | 0x997 |
6 | gtd | 0x998 | 0x9d7 |
7 | ldt0 | 0xbe0 | 0xbf7 |
8 | tss0 | 0xbf8 | 0xc5f |
9 | ldt1 | 0xc60 | 0xe77 |
10 | tss1 | 0xe78 | 0xedf |
堆栈段的分布状况
堆栈段编号 | 名称 | 起始地址 | 终止地址 |
---|---|---|---|
1 | init_stack | 0x9d8 | 0xbd8 |
2 | krn_stk0 | 0xc60 | 0xe60 |
3 | krn_stk1 | 0xe00 | 0x10e0 |
4 | user_stk1 | 0x1108 | 0x1308 |
header.s 57行至62行
pushl $0x17 # 把任务0当前局部空间数据段(堆栈段)选择符入栈
pushl $init_stack # 把堆栈指针入栈
pushfl # 把标志寄存器值入栈
pushl $0x0f # 把当前局部空间代码段选择符入栈
pushl $task0 # 把代码指针入栈
iret # 执行中断返回命令,从而切换到特权级别为3的任务0中执行
PC在iret执行后找到下一条指令的过程
执行iret之后,讲推入堆栈的段地址和偏移地址从栈里面弹出,让程序的上下文返回到发生中断的位置上。具体的实现方法就是:
- 恢复IP的值
- 恢复CS的值
- 恢复中断之前的状态
- 返回的权限发生变化
- 恢复SS、ESP
PC这之后再根据CS和IP来形成段的基地址,再加上偏移地址就得到了下一条指令的地址。
栈在iret前后发生的变化
在iret执行之前的栈如下:
在iret执行之后的栈如下:
可以看到在执行iret之前,栈顶指针为0xbc4,此时的SS值为0x10,即将跳转的地址可以看出是CS值为0x0f,IP的值为0x10e0;用户栈的栈顶地址为0x17;0x0bd8可以计算出来对应的栈顶地址位置为0xbd8的位置。正好与下方的iret执行后的表现相同。
执行之后栈顶就换成了在跳转之前指定的0x0BD8栈顶,而且程序跳转到了0x0f:0x10e0位置进行执行。这就完成了具有特权级别变换的跳转。
执行int 0x80
栈的变化
执行 int 0x80之前:
![image-20231016171414236](https://img-blog.csdnimg.cn/img_convert/2a17d460f8b541bcebfe7148967c5e15.png)
执行int 0x80之后:
在执行int 0x80之前,栈顶的位置还是指向ss:sp = 0x17:0x0BD8的位置,这个时候的cs:eip = 0x0f:0x10e9,这个时候还没有发生跳转。
在执行int 0x80之后,此时ss:esp可以看到切换到了0x10:0xe4c的位置,并且cs:eip跳转到了中断处理程序的入口也就是0x08:0x0166的位置。同时也可以观察到压入的部分是从0x17开始的,这个压入的是之前的用户栈的栈顶的值,0xe5c为ss的值,0xe58压入的是sp的值,0xe54压入的是eflags寄存器内的值,0xf代表的是对应的cs的值,0x10eb代表的原本的eip的下一个指令偏移地址位置。
所以可以看出在调用的过程中,先是将内核栈的栈顶切换到0x10:0xe4c的位置,然后将用户栈栈顶(ss:sp)、eflags寄存器的值和用户程序的返回地址(cs:eip)都压到内核的堆栈里。