MIT-JOS lab1-exercise3

本博客已经弃用,我的新博客地址:http://jujuba.me/

Exercise 3

  1. Take a look at the lab tools guide, especially the section on GDB commands. Even if you’re familiar with GDB, this includes some esoteric GDB commands that are useful for OS work.

  2. Set a breakpoint at address 0x7c00, which is where the boot sector will be loaded. Continue execution until that breakpoint. Trace through the code in boot/boot.S, using the source code and the disassembly file obj/boot/boot.asm to keep track of where you are. Also use the x/i command in GDB to disassemble sequences of instructions in the boot loader, and compare the original boot loader source code with both the disassembly in obj/boot/boot.asm and GDB.

  3. Trace into bootmain() in boot/main.c, and then into readsect(). Identify the exact assembly instructions that correspond to each of the statements in readsect(). Trace through the rest of readsect() and back out into bootmain(), and identify the begin and end of the for loop that reads the remaining sectors of the kernel from the disk. Find out what code will run when the loop is finished, set a breakpoint there, and continue to that breakpoint. Then step through the remainder of the boot loader. **

All about boot.S

Ok, now we are going to trace the boot sector, using gdb as usual. After attaching gdb to qemu, input b *0x7c00 to set breakpoint at 0x7c00, where the boot sector will be loaded.

[   0:7c00] => 0x7c00:  cli ;disable interrupt
[   0:7c01] => 0x7c01:  cld ;clear direction flag
[   0:7c02] => 0x7c02:  xor    ax,ax ;clear ax
[   0:7c04] => 0x7c04:  mov    ds,ax ;clear ds
[   0:7c06] => 0x7c06:  mov    es,ax ;clear es
[   0:7c08] => 0x7c08:  mov    ss,ax ;clear ss

First, just like we talked about before in Exercise1&&2, these are the preparation instructions. You can see the usage of them according to the comment.

[   0:7c0a] => 0x7c0a:  in     al,0x64
[   0:7c0c] => 0x7c0c:  test   al,0x2
[   0:7c0e] => 0x7c0e:  jne    0x7c0a

We can see according to the code above that these three instructions form a loop, it keeps checking the bit1 of the port 0x64, if this bit is not 1, then breaks. These instructions are used to guarantee this instructions executed before are fetched by CPU.
check here to see how the port 0x64 works.

0064    r   KB controller read status (ISA, EISA)
         bit 7 = 1 parity error on transmission from keyboard
         bit 6 = 1 receive timeout
         bit 5 = 1 transmit timeout
         bit 4 = 0 keyboard inhibit
         bit 3 = 1 data in input register is command
             0 data in input register is data
         bit 2   system flag status: 0=power up or reset  1=selftest OK
         bit 1 = 1 input buffer full (input 60/64 has data for 8042)
         bit 0 = 1 output buffer full (output 60 has data for system)
...

Evidently, this loop is trying to see whether the input buffer is full, if not, then breaks the loop.

[   0:7c10] => 0x7c10:  mov    al,0xd1
[   0:7c12] => 0x7c12:  out    0x64,al
[   0:7c14] => 0x7c14:  in     al,0x64
[   0:7c16] => 0x7c16:  test   al,0x2
[   0:7c18] => 0x7c18:  jne    0x7c14

Now the boot sector want to write 0xd1 to port 0x64, check here again, we can see things below:

D1  dbl   write output port. next byte written  to 0060
                  will be written to the 804x output port; the
                  original IBM AT and many compatibles use bit 1 of
                  the output port to control the A20 gate.

Also, the last 3 instructions above which are also a loop is trying to see if the instructions before are fetched by CPU.

[   0:7c1a] => 0x7c1a:  mov    al,0xdf
[   0:7c1c] => 0x7c1c:  out    0x60,al

This is one problem that I haven’t figured out yet, the 0xdf is send to port 0x60, but I think it is actually sent to port 0x64, I don’t know how it works.

0064    w  DF   sngl  enable address line A20 (HP Vectra only???)
[   0:7c1e] => 0x7c1e:  lgdtw  ds:0x7c64 ;load Global Descriptor Table Register
[   0:7c23] => 0x7c23:  mov    eax,cr0
[   0:7c26] => 0x7c26:  or     eax,0x1
[   0:7c2a] => 0x7c2a:  mov    cr0,eax ; set bit0 so it can change to protected mode
[   0:7c2d] => 0x7c2d:  jmp    0x8:0x7c32 ;jump to 32-bit code segment
=> 0x7c32:  mov    ax,0x10 ; ax =  0x10 = kernel data segment selector
=> 0x7c36:  mov    ds,eax  ; ds = 0x10
=> 0x7c38:  mov    es,eax  ; es = 0x10
=> 0x7c3a:  mov    fs,eax  ; fs = 0x10
=> 0x7c3c:  mov    gs,eax  ; gs = 0x10
=> 0x7c3e:  mov    ss,eax  ; ss = 0x10
=> 0x7c40:  mov    esp,0x7c00 ; use 0x7c00 as the stack top, so the stack won't grow until cover the boot sector
=> 0x7c45:  call   0x7d0a  ; jump to bootmain C function

All about main.c

While trace the main.c, I will draw the stack for you.

=> 0x7d0a:  push   ebp
=> 0x7d0b:  mov    ebp,esp ; create new frame of bootmain
=> 0x7d0d:  push   esi
=> 0x7d0e:  push   ebx ; save the values used in the caller function
=> 0x7d0f:  push   0x0 ; push parameter3 of the readseg
=> 0x7d11:  push   0x1000 ; push parameter2 of the readseg
=> 0x7d16:  push   0x10000 ; push parameter1 of the readseg

At first, we should know something about how the stack works when calling functions. Look at the picture below:

We should now that the sequence of pushing data. First, if the function has parameters, push them from right to left, then push the eip, which is the return address of the caller function, and then it executes the normal push ebp and mov ebp, esp, and finally, then local variables.

Until now, the stack should be like this:

          +------------------+  <-
          |                  |
          +------------------+  <- esp = ebp-0x14 = 0x7be4 : parameter11 of readseg
          |    0x00010000    |
          +------------------+  <- ebp-0x10 = 0x7be8 : parameter2 of readseg
          |    0x00001000    |
          +------------------+  <- ebp-0xc = 0x7bec : parameter3 of readseg
          |    0x00000000    |
          +------------------+  <- ebp-0x8 = 0x7bf0 : value of ebx
          |    0x00000000    |
          +------------------+  <- ebp-0x4 = 0x7bf4 : value of esi
          |    0x00000000    |
          +------------------+  <- ebp = 0x7bf8 : ebp of former function
          |    0x00000000    |
          +------------------+  <- 0x7bfc : ret address
          |    0x00007c4a    |
          +------------------+  <- 0x7c00 : original esp
          |     boot.S       |
          +------------------+  <-
          |                  |
          +------------------+  <- 0x00000000

We can see the return address 0x00007c4a and the original esp 0x7c00 in boot.asm:

# Set up the stack pointer and call into C.
movl    $start, %esp
  7c40: bc 00 7c 00 00          mov    $0x7c00,%esp
call bootmain
  7c45: e8 c0 00 00 00          call   7d0a <bootmain>

00007c4a <spin>:

# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
  7c4a: eb fe                   jmp    7c4a <spin>

we can see move $0x7c00, $esp and jmp 7c4a here.

=> 0x7d1b:  call   0x7cd1 ; jump to readseg
=> 0x7cd1:  push   ebp
=> 0x7cd2:  mov    ebp,esp ; create new frame of readseg
=> 0x7cd4:  push   edi ; save the value
=> 0x7cd5:  mov    edi,DWORD PTR [ebp+0xc] ; edi = [0x7ce8] = 0x1000
=> 0x7cd8:  push   esi ; save the value
=> 0x7cd9:  mov    esi,DWORD PTR [ebp+0x10] ; esi = offset = [0x7ce8] = 0
=> 0x7cdc:  push   ebx ; save the value
=> 0x7cdd:  mov    ebx,DWORD PTR [ebp+0x8] ; ebx = [0x7ce4] = 0x10000
=> 0x7ce0:  shr    esi,0x9 ; esi /= 512
=> 0x7ce3:  add    edi,ebx ; edi = 0x1000 + 0x10000 = 0x11000
=> 0x7ce5:  inc    esi ; esi = offset = 1
=> 0x7ce6:  and    ebx,0xfffffe00 ; ebx = 0x10000 & ~(512-1) = 0x10000
=> 0x7cec:  cmp    ebx,edi ; 0x10000 vs 0x11000
=> 0x7cee:  jae    0x7d02 ; if 0x10000 > 0x11000 then jump
=> 0x7cf0:  push   esi ; push offset = 1
=> 0x7cf1:  inc    esi ; offset++ -> offset = 2
=> 0x7cf2:  push   ebx ; push 0x10000
=> 0x7cf3:  add    ebx,0x200 ; pa += 512 = 0x10200
=> 0x7cf9:  call   0x7c7c ; call readsect

Now the stack looks like this:

+------------------+  <-
|                  |
+------------------+  <- ebp-0x14 = 0x7bc8 : pa = 0x10000
|    0x00010000    |
+------------------+  <- ebp-0x10 = 0x7bcc : offset = 1
|    0x00000001    |
+------------------+  <- ebp-0xc = 0x7bd0 : value of ebx = 0
|    0x00000000    |
+------------------+  <- ebp-0x8 = 0x7bd4 : value of esi = 0
|    0x00000000    |
+------------------+  <- ebp-0x4 = 0x7bd8 : value of edi = 0
|    0x00000000    |
+------------------+  <- ebp = 0x7bdc : ebp of bootmain
|    0x00007df8    |
+------------------+  <- 0x7be0 : return address
|    0x00007d20    |
+------------------+  <- 0x7be4 : former esp
| stack of bootmain|
+------------------+  <- 0x7c00

The next instructions:

=> 0x7cf9:  call   0x7c7c
=> 0x7c7c:  push   ebp
=> 0x7c7d:  mov    ebp,esp
=> 0x7c7f:  push   edi ; 0x11000
=> 0x7c80:  push   ebx ; 0x10200
=> 0x7c81:  mov    ebx,DWORD PTR [ebp+0xc] ; [0x7bcc] = offset = 1

Now the stack is :

+------------------+  <-
|                  |
+------------------+  <- ebp-0x8 = 0x7bb8 : ebx = 0x10200
|    0x00010200    |
+------------------+  <- ebp-0x4= 0x7bbc : edi = 0x11000
|    0x00011000    |
+------------------+  <- ebp=0x7bc0 : former ebp
|    0x00007bdc    |
+------------------+  <- 0x7bc4 : return address
|    0x00007cfe    |
+------------------+  <- 0x7bc8 : former esp
| stack of readseg |
+------------------+  <- 0x7be4 : former esp
| stack of bootmain|
+------------------+  <- 0x7c00

Now, we are going to trace the waitdisk function.

=> 0x7c84:  call   0x7c6a ; call waitdisk()
=> 0x7c6a:  push   ebp
=> 0x7c6b:  mov    edx,0x1f7 ; the port
=> 0x7c70:  mov    ebp,esp
=> 0x7c72:  in     al,dx ; read from the port
=> 0x7c73:  and    eax,0xffffffc0
=> 0x7c76:  cmp    al,0x40
=> 0x7c78:  jne    0x7c72 ; if the drive is not ready, keep reading from it until it is ready

Check what the bit6 of port 0x1f7 can do :

01F7    r   status register
         bit 7 = 1  controller is executing a command
         bit 6 = 1  drive is ready
         bit 5 = 1  write fault
         bit 4 = 1  seek complete
         bit 3 = 1  sector buffer requires servicing
         bit 2 = 1  disk data read successfully corrected
         bit 1 = 1  index - set to 1 each disk revolution
         bit 0 = 1  previous command ended in an error

And the stack just changes a little:

+------------------+  <-
|                  |
+------------------+  <- ebp = esp = 7x7bb0 : former ebp
|    0x00007bc0    |
+------------------+  <- 0x7bb4 : return adderss
|    0x00007c89    |
+------------------+  <- 0x7bb8 : former esp
| stack of readsect|
+------------------+  <- 0x7bc8 : former esp
| stack of readseg |
+------------------+  <- 0x7be4 : former esp

And after executing the last two instructions of waitdisk:

=> 0x7c7a:  pop    ebp
=> 0x7c7b:  ret

The stack changes back:

+------------------+  <- 0x7bb8 : former esp
| stack of readsect|
+------------------+  <- 0x7bc8 : former esp
| stack of readseg |
+------------------+  <- 0x7be4 : former esp
|       ...        |
+------------------+  <- 0x0000

And then, it will execute the following C code, and because it is not special at all, I won’t draw the stack for these code.

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();

The last several instructions are very important:

=> 0x7cbd:  mov    edi,DWORD PTR [ebp+0x8] ; [0x7bc0+0x8] = 0x10000
=> 0x7cc0:  mov    ecx,0x80 ; SECTSIZE / 4 = 0x80
=> 0x7cc5:  mov    edx,0x1f0 ; port
=> 0x7cca:  cld  ; clear the direction flag
=> 0x7ccb:  repnz ins DWORD PTR es:[edi],dx ; execute 128 times!!!
=> 0x7ccd:  pop    ebx
=> 0x7cce:  pop    edi
=> 0x7ccf:  pop    ebp
=> 0x7cd0:  ret

The most important instruction above is repnz ins DWORD PTR es:[edi],dx, it executes 128 times, what exactly does it do?
Check here to understand the prefix repnz, it mainly do this:

while (ecx != 0) {
    temp = al - *(BYTE *)edi;
    SetStatusFlags(temp);
    if (DF == 0)   // DF = Direction Flag
        edi = edi + 1;
    else
        edi = edi - 1;
    ecx = ecx - 1;
    if (ZF == 1) break;
}

Now that the ecx is 0x80 first, the we can explain why it executes 128 times.
About the ins instruction, check here.

ins instruction.
After a transfer occurs, the destination-index register is automatically incremented or decremented as determined by the value of the direction flag (DF).
The index register is incremented if DF = 0 (DF cleared by a cld instruction);
it is decremented if DF = 1 (DF set by a std instruction).
The increment or decrement count is 1 for a byte transfer, 2 for a word, and 4 for a long.
Use the rep prefix with the ins instruction for a block transfer of CX bytes or words.

It transfers a string from the port 0x1f7, which is the port of drive, to certain memory which is determined by es:[edi]. Since the ecx decreases 1 at a time, we can learn that it reads 4 bytes at a time. And the data will be transfered to the memory that starts with es:0x10000.
We can verify it before and after these instructions. When the instruction has not executed, the value at 0x10000-0x10004 is 0x00 00 00 00, however, after executing the instruction once, the memory will be changed to 0x7f 0x45 0x4c 0x46, which is .ELF in ASCII, also is the magic number of ELF header. We should be aware that the kernel image is an ELF file.

Now the programe will try to recover the stack in the caller function readseg:

+------------------+  <-
|                  |
+------------------+  <- ebp-0x8 = 0x7bb8 : ebx = 0x10200
|    0x00010200    |
+------------------+  <- ebp-0x4= 0x7bbc : edi = 0x11000
|    0x00011000    |
+------------------+  <- ebp=0x7bc0 : former ebp
|    0x00007bdc    |
+------------------+  <- 0x7bc4 : return address
|    0x00007cfe    |
+------------------+  <- 0x7bc8 : former esp
+------------------+  <- ebp-0x14 = 0x7bc8 : pa = 0x10000
|    0x00010000    |
+------------------+  <- ebp-0x10 = 0x7bcc : offset = 1
|    0x00000001    |
+------------------+  <- ebp-0xc = 0x7bd0 : value of ebx = 0
|    0x00000000    |
+------------------+  <- ebp-0x8 = 0x7bd4 : value of esi = 0
|    0x00000000    |
+------------------+  <- ebp-0x4 = 0x7bd8 : value of edi = 0
|    0x00000000    |
+------------------+  <- ebp = 0x7bdc : ebp of bootmain
|    0x00007df8    |
+------------------+  <- 0x7be0 : return address
|    0x00007d20    |
+------------------+  <- 0x7be4 : former esp
| stack of bootmain|
+------------------+  <- 0x7c00

Then the instructions are:

=> 0x7ccd:  pop    ebx ; 0x10200
=> 0x7cce:  pop    edi ; 0x11000
=> 0x7ccf:  pop    ebp ; 0x7bdc
=> 0x7cd0:  ret    ; return 0x7cfe
=> 0x7cfe:  pop    eax ; 0x10000
=> 0x7cff:  pop    edx ; 0x01
=> 0x7d00:  jmp    0x7cec ; while (pa < end_pa)

And then the stack is exactly recovered:

+------------------+  <- ebp-0xc = 0x7bd0 : value of ebx = 0
|    0x00000000    |
+------------------+  <- ebp-0x8 = 0x7bd4 : value of esi = 0
|    0x00000000    |
+------------------+  <- ebp-0x4 = 0x7bd8 : value of edi = 0
|    0x00000000    |
+------------------+  <- ebp = 0x7bdc : ebp of bootmain
|    0x00007df8    |
+------------------+  <- 0x7be0 : return address
|    0x00007d20    |
+------------------+  <- 0x7be4 : former esp
| stack of bootmain|
+------------------+  <- 0x7c00

Then it executes this code again:

=> 0x7cec:  cmp    ebx,edi ; while (pa < end_pa)
=> 0x7cee:  jae    0x7d02 ; if pa is greater, break
=> 0x7cf0:  push   esi ; push parameter2, offset = 0x1
=> 0x7cf1:  inc    esi ; offset++
=> 0x7cf2:  push   ebx ; push parameter1, pa = 0x10200
=> 0x7cf3:  add    ebx,0x200 ; pa += 0x200

To sum up, we can see what the code above is doing, read count(0x1000)
bytes from kernel to memory starts from pa(physical address : 0x10000) to end_pa(end of physical address : 0x11000), and the offset represents the number of sector that the program is going to read.

After reading 10 times from the disk, the kernel is now read completely, and the following instructions are executed.

=> 0x7cec:  cmp    ebx,edi ; 0x11000 = 0x11000
=> 0x7cee:  jae    0x7d02 ; jump to end of readseg
=> 0x7d02:  lea    esp,[ebp-0xc] ; make esp = ebp
=> 0x7d05:  pop    ebx ; 0x0
=> 0x7d06:  pop    esi ; 0x0
=> 0x7d07:  pop    edi ; 0x0
=> 0x7d08:  pop    ebp ; 0x7df8
=> 0x7d09:  ret ; jump to 0x7d20
=> 0x7d20:  add    esp,0xc ; balance the stack, skip the 3 parameters of `readseg`

Before these instructions, the stack looks like this:

+------------------+  <- ebp-0xc = 0x7bd0 : value of ebx = 0
|    0x00000000    |
+------------------+  <- ebp-0x8 = 0x7bd4 : value of esi = 0
|    0x00000000    |
+------------------+  <- ebp-0x4 = 0x7bd8 : value of edi = 0
|    0x00000000    |
+------------------+  <- ebp = 0x7bdc : ebp of bootmain
|    0x00007df8    |
+------------------+  <- 0x7be0 : return address
|    0x00007d20    |
+------------------+  <- 0x7be4 : former esp
| stack of bootmain|
+------------------+  <- 0x7c00

After these instructions, the stack will be changed to the stack of bootmain:

          +------------------+  <- esp=ebp-0x8 = 0x7bf0 : value of ebx
          |    0x00000000    |
          +------------------+  <- ebp-0x4 = 0x7bf4 : value of esi
          |    0x00000000    |
          +------------------+  <- ebp = 0x7bf8 : ebp of former function
          |    0x00000000    |
          +------------------+  <- 0x7bfc : ret address
          |    0x00007c4a    |
          +------------------+  <- 0x7c00 : original esp
          |     boot.S       |
          +------------------+  <-
          |                  |
          +------------------+  <- 0x00000000

Here comes the last part of bootmain.

=> 0x7d23:  cmp    DWORD PTR ds:0x10000,0x464c457f ; check if the kernel is a valid ELF
=> 0x7d2d:  jne    0x7d67 ; if not, jump to bad
=> 0x7d2f:  mov    eax,ds:0x1001c ; mov e_phoff to eax
=> 0x7d34:  lea    ebx,[eax+0x10000] ; mov the start of the program header table to ebx = ph = 0x10034
=> 0x7d3a:  movzx  eax,WORD PTR ds:0x1002c ; mov e_phnum to eax = 0x03
=> 0x7d41:  shl    eax,0x5 ; multiply 0x20 = 0x60
=> 0x7d44:  lea    esi,[ebx+eax*1] ; esi = eph = 0x10094
=> 0x7d47:  cmp    ebx,esi ; 0x10034 vs 0x10094
=> 0x7d49:  jae    0x7d61 ; break
=> 0x7d4b:  push   DWORD PTR [ebx+0x4] ; [0x10038] = p_offset = 0x1000
=> 0x7d4e:  add    ebx,0x20 ; ebx = ph = 10054
=> 0x7d51:  push   DWORD PTR [ebx-0xc] ; [0x10048] = p_memsz = 0x72ca
=> 0x7d54:  push   DWORD PTR [ebx-0x14] ; [0x10040] = p_pa = 0x10000
=> 0x7d57:  call   0x7cd1 ; call read seg

The ELF file starts with .ELF, which is 0x7f 0x45 0x4c 0x46 in hex, check all about ELF format here.

The e_phoff :Points to the start of the program header table. It usually follows the file header immediately, making the offset 0x34 or 0x40 for 32- and 64-bit ELF executables, respectively.

The e_phnum : Contains the number of entries in the program header table.


At last, we need to answer these four questions, after all the content above, I think these will be easy for us.

  • At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?

After loading the Global Descriptor Table Register, and set the bit0 of cr0 to 1(check here), then jump to a 32-bit address using pattern like address : address.

  • What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

  • Where is the first instruction of the kernel?

  • 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?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值