relevant source:
1、 Brennan’s Guide to Inline Assembly
http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html
2、 Intel 80386 Programmer’s Reference Manual
http://www.logix.cz/michal/doc/i386/
一张重点图: Physical address space,其中low memory部分由于历史原因(一开始空间小),同样也是RAM。BIOS ROM作用是需要理解的核心部分。
What happens at booting?
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
1、The IBM PC starts executing at physical address 0x000ffff0, which is at the very top of the 64KB area reserved for the ROM BIOS.
2、The PC starts executing with CS = 0xf000 and IP = 0xfff0.
3、The first instruction to be executed is a jmp instruction, which jumps to the segmented address CS = 0xf000 and IP = 0xe05b.
补充知识,4个段寄存器:
CS(Code segment)指向代码段,代码段用于储存程序的指令;
DS(Data Segment)指向数据段,数据段用于暂存原始数据和处理后的中间结果及最终结果;
SS(Stack Segment)指向堆栈段,堆栈段用于形成堆栈区;
ES(Extra Segment)指向扩展段,扩展段与数据段类似,一般情况下,数据段用于储存局部变量,扩展段用于储存全局变量。
补充知识,CPU用CS:IP寻找下一条指令, jmp指令可以修改IP的值??该系列课程采用QEMU emulator模拟真实的PC(该PC使用8088 processor)。处理器进入real mode并且采用如下寻址方式:physical addr = 16*CS + PC
接下来通过stepi看看BIOS都做了些什么:
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
/* check whether what is in addr 0xf6ac8 = 0*/
[f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8
[f000:e062] 0xfe062: jne 0xfd2e1
/* set dx = 0, set ss = 0 */
[f000:e066] 0xfe066: xor %dx,%dx
[f000:e068] 0xfe068: mov %dx,%ss
/* stack pointer: esp = 0 => esp = 0x7000 */
[f000:e06a] 0xfe06a: mov $0x7000,%esp
/* edx = 0xf34c2 */
[f000:e070] 0xfe070: mov $0xf34c2,%edx
[f000:e076] 0xfe076: jmp 0xfd15c
[f000:d15c] 0xfd15c: mov %eax,%ecx
cli和cld两条指令的作用看了下别人博客的解释
/*关闭中断指令。启动时的操作是比较关键不能被中断。这个关中断指令用于关闭那些可以屏蔽的中断。比如大部分硬件中断 */
[f000:d15f ] 0xfd15f: cli
/* 使方向标志DF为0,表示后续的串操作比如MOVS操作,内存地址的变化方向,如果为0代表从低地址值变为高地址 */
[f000:d160] 0xfd160: cld
/* PC机给每一个端口分配了一个地址,所有端口成线性排列,形成一个独立于内存空间的I/O地址空间,一般用十六进制表示。
端口链接:
http://bochs.sourceforge.net/techspec/PORTS.LST
OUT 和 IN 在汇编中是端口读写操作指令。端口是主机与外设进行数据交换使用的,分为数据端口,状态端口和控制端口三种。
例如:
IN AL, 21H 表示从21H端口读一个字节数据到AL;
OUT 21H,AL 表示将AL持有的数据写入21H端口
*/
[f000:d161] 0xfd161: mov $0x8f,%eax
[f000:d167] 0xfd167: out %al,$0x70
[f000:d169] 0xfd169: in $0x71,%al
/* 据说这里是打开A20地址线,相关内容我没有深入了解,是比较底层的东西了,暂时不需要用到:
A20地址线并不是打开保护模式的关键,只是在保护模式下,不打开A20地址线,你将无法访问到所有的内存 */
[f000:d16b] 0xfd16b: in $0x92,%al //here al = 0 (print /x $al)
[f000:d16d] 0xfd16d: or $0x2,%al
[f000:d16f] 0xfd16f: out %al,$0x92
/* lidtw lgdtw两条命令就是加载idtr gdtr寄存器
(https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce-74.html)
(https://blog.csdn.net/chen1540524015/article/details/74076468)
*/
[f000:d171] 0xfd171: lidtw %cs:0x6ab8
[f000:d177] 0xfd177: lgdtw %cs:0x6a74
/* CR0的位0是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。 */
[f000:d17d] 0xfd17d: mov %cr0,%eax
[f000:d180] 0xfd180: or $0x1,%eax
[f000:d184] 0xfd184: mov %eax,%cr0
[f000:d187] 0xfd187: ljmpl $0x8,$0xfd18f
After executing BIOS part, it will search for and read the boot loader (in hard disk or CD-ROM, here boot loader is a program consisting of boot/main.c and boot/boot.S) and transfer control to it.
exercise 3
Basic knowledge:
bootable disk: boot loader is in the 1st sector , kernel image(in ELF format) is in the 2nd sector of the bootable disk.
A sector is the disk’s minimum transfer granularity(512 bytes)
When the BIOS finds a bootable floppy or hard disk, it loads the 512-byte boot sector into memory at physical addresses 0x7c00 through 0x7dff, and then uses a jmp instruction to set the CS:IP to 0000:7c00, passing control to the boot loader.
First read boot/main.c source code
ELFHDR
Proghdr
outw() write to given I/O port
inw() read from given I/O port and print the value
struct elfhdr {
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry; // 程序入口的虚拟地址
uint phoff; // program header 表的位置偏移
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum; //program header表中的入口数目
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
```c
struct proghdr {
uint type; // 段类型
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;
uint filesz;
uint memsz; // 段在内存映像中占用的字节数
uint flags;
uint align;
};
————————————————
版权声明:本文为CSDN博主「qqNCer」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qqNCer/article/details/105984272
#include <inc/x86.h>
#include <inc/elf.h>
/**********************************************************************
* This a dirt simple boot loader, whose sole job is to boot
* an ELF kernel image from the first IDE hard disk.
*
* DISK LAYOUT
* * This program(boot.S and main.c) is the bootloader. It should
* be stored in the first sector of the disk.
*
* * The 2nd sector onward holds the kernel image.
*
* * The kernel image must be in ELF format.
*
* BOOT UP STEPS
* * when the CPU boots it loads the BIOS into memory and executes it
*
* * the BIOS intializes devices, sets of the interrupt routines, and
* reads the first sector of the boot device(e.g., hard-drive)
* into memory and jumps to it.
*
* * Assuming this boot loader is stored in the first sector of the
* hard-drive, this code takes over...
*
* * control starts in boot.S -- which sets up protected mode,
* and a stack so C code then run, then calls bootmain()
*
* * bootmain() in this file takes over, reads in the kernel and jumps to it.
**********************************************************************/
#define SECTSIZE 512
#define ELFHDR ((struct Elf *) 0x10000) // scratch space
void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);
//bootmain() does not return??
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// read 1st page off disk, store it at 0x10000
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
// phoff is offset of program header table
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
uint32_t end_pa;
end_pa = pa + count;
// round down to sector boundary
// ~(SECTSIZE-1) = 1...1000000000
pa &= ~(SECTSIZE - 1);
// translate from bytes to sectors, and kernel starts at sector 1, 第一次调用时offset = 0+1 = 1
offset = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
while (pa < end_pa) {
// Since we haven't enabled paging yet and we're using
// an identity segment mapping (see boot.S), we can
// use physical addresses directly. This won't be the
// case once JOS enables the MMU.
readsect((uint8_t*) pa, offset);
pa += SECTSIZE;
offset++;
}
}
void
waitdisk(void)
{
// wait for disk reaady
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
void
readsect(void *dst, uint32_t offset)
{
// wait for disk to be ready
waitdisk();
//outb(data , port) will put data into port(I/O addr)
outb(0x1F2, 1); // count = 1
outb(0x1F3, offset);
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE/4);
}
大致看一下这部分c code做了什么事,接下来就是按照exercise3的调试指导,逐步查看源码,stepi,x/ni:
常用的端口地址链接:
http://www.os2site.com/sw/info/memory/ports.txt
064 Extended PPI PS/2 8042 Keyboard, Aux. Device Controller
//cli:clear interrupt flag关闭终端,cld:clear direction flag,
=> 0x7c00: cli
0x7c01: cld
//set ds, es ss to 0
0x7c02: xor %ax,%ax
0x7c04: mov %ax,%ds
0x7c06: mov %ax,%es
0x7c08: mov %ax,%ss
//value at 0x64 to %al, a loop which makes al&0x2 = 0x0
//下面的链接基本上解答了端口0x64是什么以及这里循环的意义是什么,简单说来就是0x64端口是用来发送命令到PS/2键盘控制器(0x60向/从发送数据),而bit 1(0x2)对应的是input buffer status(empty if 0, full if 1)
//https://www.thinbug.com/q/21078932
//下面的链接比较全面
//https://www.win.tue.nl/~aeb/linux/kbd/scancodes-11.html
0x7c0a: in $0x64,%al //这里print出来al = 0x1c
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
//send 0xd1 to port 0x64
//Command 0xd1: Write output port,
//Write the output port (P2).
//Note that writing a 0 in bit 0 will cause a hardware reset.
0x7c10: mov $0xd1,%al
0x7c12: out %al,$0x64
//same loop like above
0x7c14: in $0x64,%al
0x7c16: test $0x2,%al
0x7c18: jne 0x7c14
//send 0xdf to port 0x60
//Command 0xdf: Enable A20 address line
//segment<<4 + offset
//下面链接解释了A20 address line的作用,实际上8088/8086只有二十根地址线,但是80286有24根,为了兼容,所以需要使能A20
//https://www.zhihu.com/question/29375534
0x7c1a: mov $0xdf,%al
0x7c1c: out %al,$0x60
//transfer from real mode to protected mode,两者简介如下链接
//
//load global descriptor table, put relevant info(length and begin addr of gdt)into gdtr(a 48-bit register)
//boot.S最后有两个标识符gdt和gdtdsec
0x7c1e: lgdtw 0x7c64 //boot.S里对应lgdt gdtdsec
//cr0 register bit_0 = 1 protected mode
0x7c23: mov %cr0,%eax
0x7c26: or $0x1,%eax
0x7c2a: mov %eax,%cr0
//一个简单的跳转指令,把当前的运行模式切换成32位地址模式
0x7c2d: ljmp $0x8,$0x7c32
//Set up the protected-mode data segment registers
0x7c32: mov $0xd88e0010,%eax
0x7c38: mov %ax,%es
0x7c3a: mov %ax,%fs
---Type <return> to continue, or q <return> to quit---return
0x7c3c: mov %ax,%gs
0x7c3e: mov %ax,%ss
//set up the stack pointer and then call bootmain()
0x7c40: mov $0x7c00,%sp
0x7c43: add %al,(%bx,%si)
0x7c45: call 0x7d0a
//since bootmain() does not return if works well, it won't be executed
0x7c48: add %al,(%bx,%si)
//%ebp, %esi 入栈保存,callee
0x7d0a: push %ebp
0x7d0b: mov %esp,%ebp
0x7d0d: push %esi
0x7d0e: push %ebx
//分别存储在 0x7bec, 0x7be8, 0x7be4,入参
0x7d0f: push $0x0
0x7d11: push $0x1000
0x7d16: push $0x10000
//call readseg
0x7d1b: call 0x7cd1
......
//接下来直接跳转到readseg()
0x7cd1: push %ebp
//%esp = 0x7bdc, %ebp = 0x7bdc
=> 0x7cd2: mov %esp,%ebp
//store %edi,要恢复
0x7cd4: push %edi
//set %edi = 0x1000,存储argument 2 ——count
0x7cd5: mov 0xc(%ebp),%edi
//set %esi = 0 ,存储argument 3 ——offset
0x7cd8: push %esi
0x7cd9: mov 0x10(%ebp),%esi
0x7cdc: push %ebx
//set %ebx = 0x10000 存储argument 1 ——pa
0x7cdd: mov 0x8(%ebp),%ebx
//%esi = (offset / SECTSIZE) + 1
0x7ce0: shr $0x9,%esi
0x7ce3: add %ebx,%edi //%edi = count + pa
0x7ce5: inc %esi
//%ebx (pa) round down to nearset sector boundary
0x7ce6: and $0xfffffe00,%ebx
//if (%ebx >= %edi) jump to 0x7d02, loop over
0x7cec: cmp %edi,%ebx
0x7cee: jae 0x7d02
0x7cf0: push %esi
0x7cf1: inc %esi //%esi ++ (argument offset)
0x7cf2: push %ebx
0x7cf3: add $0x200,%ebx //%ebx(argument pa) + 512
//call readsect()
0x7cf9: call 0x7c7c
//
0x7cfe: pop %eax
0x7cff: pop %edx
0x7d00: jmp 0x7cec
//%esp = %ebp - 0xc
0x7d02: lea -0xc(%ebp),%esp
0x7d05: pop %ebx
---Type <return> to continue, or q <return> to quit---return
0x7d06: pop %esi
0x7d07: pop %edi
0x7d08: pop %ebp
0x7d09: ret
//readsect()
=> 0x7c7c: push %ebp
// %ebp = 0x7bc0
0x7c7d: mov %esp,%ebp
0x7c7f: push %edi //
0x7c80: push %ebx //
0x7c81: mov 0xc(%ebp),%ebx // %ebx = 1 = offset??
//call waitdisk()
0x7c84: call 0x7c6a
//IO地址 功能
0x1f0 读数据,当0x1f7不为忙状态时,可以读。
0x1f2 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
0x1f3 如果是LBA模式,就是LBA参数的0-7位,0号硬盘扇区数
0x1f4 如果是LBA模式,就是LBA参数的8-15位,0号硬盘柱面(低字节)
0x1f5 如果是LBA模式,就是LBA参数的16-23位,0号硬盘柱面(高字节)
0x1f6 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据
//here comes outb part——write to I/O port
//%ebx stores argument offset
//outb port_no data => put data into (port_no);()是取地址的意思
//设置读取扇区的数目为1
=> 0x7c89: mov $0x1f2,%edx //%edx = 0x1f2 (port)
0x7c8e: mov $0x1,%al //%al = 0x1
0x7c90: out %al,(%dx) //(0x1f2) = 0x1
//(0x1f3) = %bl offset 0~7位
0x7c91: movzbl %bl,%eax //%eax = %bl
0x7c94: mov $0xf3,%dl //%dl = 0xf3
0x7c96: out %al,(%dx)
//(0x1f4) = %bh (%ebx >> 8) 8~15位
0x7c97: movzbl %bh,%eax //movzbl will padd other bits = 0
0x7c9a: mov $0xf4,%dl
0x7c9c: out %al,(%dx)
//(0x1f5) = (%ebx >> 16) 16~23位
0x7c9d: mov %ebx,%eax
0x7c9f: mov $0xf5,%dl
0x7ca1: shr $0x10,%eax
0x7ca4: movzbl %al,%eax
0x7ca7: out %al,(%dx)
//(0x1f6) = (offset>>24)
0x7ca8: shr $0x18,%ebx //offset>>24
0x7cab: mov $0xf6,%dl
0x7cad: mov %bl,%al
0x7caf: or $0xffffffe0,%eax //29~31位强制设为1
0x7cb2: out %al,(%dx)
//0x20命令,读取扇区
0x7cb3: mov $0x20,%al
0x7cb5: mov $0xf7,%dl
0x7cb7: out %al,(%dx)
//那么输入完上述地址,指令到相应的端口后,就可以让磁盘自己去工作,
//此时系统只需调用waitdisk过程来等待磁盘完成读取。waitdisk退出后,
//代表数据已经被读取。然后就可以执行下一个指令了。
0x7cb8: call 0x7c6a
0x7cbd: mov 0x8(%ebp),%edi
0x7cc0: mov $0x80,%ecx
0x7cc5: mov $0x1f0,%edx
0x7cca: cld
0x7ccb: repnz insl (%dx),%es:(%edi)
0x7ccd: pop %ebx
0x7cce: pop %edi
0x7ccf: pop %ebp
0x7cd0: ret
//insl()
0x7cbd: mov 0x8(%ebp),%edi
0x7cc0: mov $0x80,%ecx
=> 0x7cc5: mov $0x1f0,%edx
//清除方向标识,这个在前面讨论过,主要是为了能够实现串操作
//串操作的含义就是连续的一串相同的操作,通常作用在连续的内存上,
//比如把一串字符串常量送入到某个连续地址处,此时如果采用串操作的话,
//每传一个字节的数据,串操作可以自动的把源操作数和目的操作数的地址加
//或减1。那么下一个操作就直接作用在下一个空间了
0x7cca: cld
//repnz() :repnz指令又叫做重复串操作指令,它是一个前缀,
//位于一条指令之前,这条指令将会一直被重复执行,
//并且直到计数寄存器的值满足某个条件。repnz指令是当计数器%ecx的值
//不为零是就一直重复后面的串操作指令。那么被重复调用的指令就是insl指令。
//%dx = 0x1f0, %edi = (%ebp + 8)
0x7ccb: repnz insl (%dx),%es:(%edi)
//restore value , return to readseg()
0x7ccd: pop %ebx
0x7cce: pop %edi
0x7ccf: pop %ebp
0x7cd0: ret
0x7cfe: pop %eax
0x7cff: pop %edx
0x7d00: jmp 0x7cec
//%edi = 0x11000, %ebx = 0x10200 (pa + 512)
//loop until %ebx >= %edi (end_pa)
0x7cec: cmp %edi,%ebx
0x7cee: jae 0x7d02
// after 8 loop , the 1st page is read off disk
// now it returns to bootmain()
0x7d02: lea -0xc(%ebp),%esp
0x7d05: pop %ebx
0x7d06: pop %esi
0x7d07: pop %edi
0x7d08: pop %ebp
0x7d09: ret
0x7d0a: push %ebp
后面的内容大同小异,我们已经将ELFHDR load 进了 0x10000处,接下来首先判断load的内容是否对也就是是否ELFHDR->e_magic != ELF_MAGIC
其次读取字段ph_off,判断Program Header相对于ELFHDR的位置
然后,读取字段ph_num(有几个program segment),然后进入循环:
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
//每个表项是32个字节大小,所以ph每次相当于+32
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();
重点是理解,boot/main.c中readseg()的描述
// Read ‘count’ bytes at ‘offset’ from kernel into physical address ‘pa’.
// Might copy more than asked
该函数中之所以offset = (offset / SECTSIZE) + 1有+1这一步是因为kernel image就是从第二个section开始的(也可以说是section 1)
理解了这一步就可以理解下面这一步的意义了:
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
这里引用别人博客的描述,在此不再赘述了(链接也附上):
这一个步操作的意思:
ph当前存放的是一个Program Header Table中一个表项的起始地址。ph->p_pa字段就是p_paddr字段,代表这个段的将要被存放在这个系统的内存中的起始物理地址。ph->p_memsz字段,代表这个段被实际的装入内存后,它所占用的内存大小。ph->p_offset字段,代表这个段的起始地址距离整个内核文件起始地址的偏移。所以这个C语言语句的含义就是把这个表项所代表的段存放到ph->p_pa字段的所指定的内存地址处。
至于汇编程序也和前面在bootmain刚开始的时候调用readseg函数的方式类似。0x7d4b~0x7d51是在把readseg的输入参数压入栈中。0x7d54是修改ph指针,让它指向下一个Program Header Table中的表项。0x7d57调用readseg函数。0x7d5c指令是修改%esp,因为之前在输入参数时一共输入3个长度为4字节的参数,所以%esp减少了0x0c,这里让%esp的值恢复。0x7d5f~0x7d61 判断循环条件。
然后就是循环执行完成的操作了,循环执行完成后,操作系统内核中所有的指令,数据都已经转移到内存中。下面就要执行最后一步的操作:
((void ()(void)) (ELFHDR->e_entry))();
其中ELF文件头的e_entry字段的含义是这个可执行文件的第一条指令的虚拟地址。所以这句话的含义就是把控制权转移给操作系统内核*
https://www.cnblogs.com/fatsheep9146/p/5115086.html
Q&A:
The last instruction which boot loader executed is
call *0x10018
And the instruction at 0x10018 is the first instruction of kernel, it is
movw $0x1234, 0x472
Where is the first instruction of the kernel?
at *0x10018 which is 0x10000c.
How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?
Remember that kernel is a set of some programs, and that kernel iamge is stored in sector 1(2nd sector) of the disk and kernel image is an ELF file, and in each ELF-format file, there is an ELF header which indicates the size of this ELF file.
In JOS, The code below does this thing:
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;