MIT6.828——Lab1 partB(麻省理工操作系統課程實驗)


Lab1

歷時2天,完成了LAB1,完整代碼倉庫可點擊:https://github.com/Elio-yang/MIT6.828

partA 練習

  • exercise3

gdb指令:

x/Ni addr :反匯編addr處的N條指令

x/Nx addr:打印N字節addr處的內存

b *addr:在addr處設置斷點

readsect(): 0x7c7c

bootmain():0x7d25

循環結束的第一條指令是0x7d81處的call *0x10018,利用gdb0x10018內存處的值為0x10000c,故第一條指令是call 0x10000c。這個地址就是kernel的entry。

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

ljmp $PROT_MODE_CSEG,$protcseg這條指令后開始執行32位代碼。真正造成切換的,是CR0PE位被置為,進入了保護模式。

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

last:

 call *0x10018

first:

f010000c <entry>:
f010000c:	66 c7 05 72 04 00 00 	movw   $0x1234,0x472

Where is the first instruction of the kernel?

很顯然在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?

都是通過ELF header得知的。

Loading the kernel

第一個需要注意的是代碼的鏈接地址和裝載地址

使用命令

objdump -h <file.o>
# -x Display all available header information 
# -f Display entry point
# 更多用法 man objdump 即可

在kernel中這兩者是不同的,但是在之前的boot中

二者是一致的。在kern/entry.S中有這樣一段代碼

# Turn on paging.
movl	%cr0, %eax
orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
movl	%eax, %cr0

這便開啟了地址映射,在此之前kernel的VMA和LMA地址處的內存一般是不同的,但是開啟分頁之后,LMA映射到了VMA。

The Kernel

第一個值得注意的是:開啟分頁模式,將虛擬地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)(/kern/entry.S)

分頁模式下的尋址,在Intel手冊中也有給出

開啟這個模式的代碼如下

# Load the physical address of entry_pgdir into cr3.  entry_pgdir
# is defined in entrypgdir.c.
movl	$(RELOC(entry_pgdir)), %eax
movl	%eax, %cr3
# Turn on paging.
movl	%cr0, %eax
orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
movl	%eax, %cr0

關於地址的映射在kern/entrypgdir.c有代碼實現

__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
	// Map VA's [0, 4MB) to PA's [0, 4MB)
	[0]
		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
	// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
	[KERNBASE>>PDXSHIFT]
		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

編譯器分配的空間是強制性4kB頁對齊的。pgdir是一個1024項的數組。這里可以不用詳細了解原理For now, you don't have to understand the details of how this works, just the effect that it accomplishes.

  • exercise7

在開啟分頁之前,這兩個地址內容是不一致的,后面經過了地址映射,這兩者的內容一致。注釋掉

movl %eax, %cr0程序會崩潰。

Formated Printing to the Console

首先是幾個函數的調用關系

然后練習題

  • exercise8

這個文件就是lib/printfmt.c

// (unsigned) octal
case 'o':
    // Replace this with your code.
    num=getuint(&ap,lflag);
    base=8;
    goto number;

對照上下文很容易補全。

下面是回答問題:

Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?

對照上文調用關系圖即可

Explain the following from console.c

if (crt_pos >= CRT_SIZE) {
    int i;
    memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) *sizeof(uint16_t));
    for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
        crt_buf[i] = 0x0700 | ' ';
    crt_pos -= CRT_COLS;
}

首先文本模式最多能顯示25*80個字符,即25行每行80個。此處

// console.h
#define CRT_ROWS	25
#define CRT_COLS	80
#define CRT_SIZE	(CRT_ROWS * CRT_COLS)

因此,這一段處理的是超出一屏幕以后的做法:即舍棄最上面一行,整體上移一行。

For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC's calling convention on the x86.

Trace the execution of the following code step-by-step:

int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
  • In the call to cprintf(), to what does fmt point? To what does ap point?
  • List (in order of execution) each call to cons_putc, va_arg, and vcprintf. For cons_putc, list its argument as well. For va_arg, list what ap points to before and after the call. For vcprintf list the values of its two arguments.

GCC 函數調用約定是參數從右往左入棧。此處fmt指向的就是第一個參數的位置。而ap指向第一個可變參數,也就是第二個參數x的位置。關於變參數,JOS使用的是GCC builtin來實現的。其實現可以用如下代碼進行大致說明(不是嚴謹的完整實現):

#define va_start(list,param_1st)   ( list = (va_list)&param1+ sizeof(param_1st) )
#define va_arg(list,type)   ( (type *) ( list += sizeof(type) ) )[-1]
#define va_end(list) ( list = (va_list)0 )

因此:

va_list:即char*

va_start:獲取第一個可變參數的地址

va_arg:返回指向下一個參數的指針

va_end:清空參數列表

Run the following code.

    unsigned int i = 0x00646c72;
    cprintf("H%x Wo%s", 57616, &i);

What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise.

Here's an ASCII tablethat maps bytes to characters.

The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here's a description of little- and big-endian and a more whimsical description.

把這段代碼加入init.c中,運行make qemu,結果如下

0xe110=57616這很好解釋,查閱ASCII表,得知

00(\0) 64(d) 6c(l) 72(r)

顯然這是由於小端模式而使用的一個數。為了證明這一點,可以輸出&i內存處的字節。將下面這段代碼放在上面打印代碼的后面

cprintf("addr of i: %p\n",&i);
char *p=(char*)&i;
for(int i=0;i<4;i++){
	cprintf("[%x]",*p);
	p++;
}

輸出結果如下:

In the following code, what is going to be printed after'y='? (note: the answer is not a specific value.) Why does this happen?

 cprintf("x=%d y=%d", 3);

運行結果如下

顯然y的值並不一定固定,他就是把內存中那個位置的數拿來充當了第二個參數。

Let's say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

更改了入棧方式,相應地更改va_startva_start即可。

The Stack

先看這個練習

  • exercise9

entry.S中可以找到如下代碼

# where the stack is set.
# Clear the frame pointer register (EBP)
# so that once we get into debugging C code,
# stack backtraces will be terminated properly.
movl	$0x0,%ebp			# nuke frame pointer
# Set the stack pointer
movl	$(bootstacktop),%esp
# now to C code
call	i386_init

利用gdb得知,movl $(bootstacktop),%esp會被編譯為movl $0xf0110000,%esp。因此棧何時初始化,棧放在哪兒都清楚了。繼續看代碼

###################################################################
# boot stack
###################################################################
	.p2align	PGSHIFT		# force page alignment
	.globl		bootstack
bootstack:
	.space		KSTKSIZE
	.globl		bootstacktop   

這便開辟了棧的大小,即32KB。棧由高地址向低地址增長。

下面,關於函數的調用過程,做一個總結,可以參考[CSAPP,p164]。

這是從課件ppt截取的兩頁

關於函數的調用,一般有如下的動作發生:

  1. 函數調用者(caller)將參數入棧,按照從右到左的順序入棧
  2. call指令會自動將當前%eip(指向call的后面一條指令)入棧,ret指令將自動從棧中彈出該值到eip寄存器
  3. 被調用函數(callee)負責:將%ebp入棧,%esp的值賦給%ebp。

因此函數開頭都會是類似的兩條指令

push %ebp
mov %esp,%ebp

因此整個調用鏈差不多可以描述成如下形式

來到下一個練習

  • exercise10

每次call之后會干什么,上文已經分析了。至於每次遞歸入棧的字,偽代碼可以表示為

push %eip
push %ebp
push %esi
push %ebx

共計0x10B。

  • exercise11

需要我們更改mom_backtrace()函數,達到的效果如下:

題目中已經說明,獲得%ebp的函數就是read_ebp()。那么編碼工作應該很好完成了(利用調用鏈中%ebp的鏈)

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
	// Your code here.
	uint32_t *ebp=(uint32_t*)read_ebp();
	while(ebp!=NULL){
	    cprintf("ebp %8x  eip %8x  args %08x %08x %08x %08x %08x\n",
			ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6]);
	    ebp=(uint32_t *)(*ebp);
	}
	return 0;
}

運行結果如下

  • exercise12

練習12的任務有三個:

  1. 搞清楚__STAB_*
  2. 添加命令backtrace
  3. 完善mon_backtrace
  • 任務一

    根據提示,查看這幾個文件,首先是kernel.ld

    .stab : {
                    PROVIDE(__STAB_BEGIN__ = .);
                    *(.stab);
                    PROVIDE(__STAB_END__ = .);
                    BYTE(0)         /* Force the linker to allocate space
                                       for this section */
            }
     
    .stabstr : {
                    PROVIDE(__STABSTR_BEGIN__ = .);
                    *(.stabstr);
                    PROVIDE(__STABSTR_END__ = .);
                    BYTE(0)         /* Force the linker to allocate space
                                       for this section */
            }
    

    可以知道.stab.stabstr應該是兩個段。

    接着 objdump -h obj/kern/kernel

然后是``objdump -G obj/kern/kernel``

執行后面的操作以后,大致可以知道這是一個段,包含了調試信息(符號表)。細節可以不用太了解。接着找到``stab.h``,其中

這兩項便是后文編碼尋找行號時需要的。下面開始任務二和三
  • 任務二

    題目中提示了需要使用debuginfo_eip,查找這個函數發現,他會將需要的信息存到類型為struct Eipdebuginfo的結構體中。查看該結構體定義(kern/kdebebug.h)

    // Debug information about a particular instruction pointer
    struct Eipdebuginfo {
    	const char *eip_file;		// Source code filename for EIP
    	int eip_line;			    // Source code linenumber for EIP
    
    	const char *eip_fn_name;	// Name of function containing EIP
    					            //  - Note: not null terminated!
        
    	int eip_fn_namelen;		// Length of function name
    	uintptr_t eip_fn_addr;		// Address of start of function
    	int eip_fn_narg;		// Number of function arguments
    };
    

    因此只需要使用debuginfo_eip填充該結構體,再輸出信息即可。

    static struct Command commands[] = {
    	{ "help", "Display this list of commands", mon_help },
    	{ "kerninfo", "Display information about the kernel", mon_kerninfo },
    	{ "backtrace", "Show stack backtrace",mon_stacktrace}	
    };
    //......
    int 
    for_stack(int argc,char **argv,struct Trapframe *tf)
    {
    	uint32_t *ebp=(uint32_t*)read_ebp();
    	while(ebp!=NULL){
    	    struct Eipdebuginfo info;
    	    uint32_t eip = ebp[1];
    	    debuginfo_eip((int)eip, &info);
    	    cprintf("  ebp %8x  eip %8x  args %08x %08x %08x %08x %08x\n",
    			ebp,ebp[1],ebp[2],ebp[3],ebp[4],ebp[5],ebp[6]);
    	    const  char* filename=(&info)->eip_file;
    	    int line = (&info)->eip_line;
    	    const char * not_null_ter_fname=(&info)->eip_fn_name;
    	    int offset = (int)(eip)-(int)((&info)->eip_fn_addr);
    	    cprintf("        %s:%d:  %.*s+%d\n",filename,line,info.eip_fn_namelen,not_null_ter_fname,offset);
    	    ebp=(uint32_t *)(*ebp);
    	}
    	return 0;
    }
    int
    mon_stacktrace(int argc,char **argv,struct Trapframe *tf)
    {
        cprintf("Stack backtrace:\n");
        return for_stack(argc,argv,tf);
    }
    

    其中關於文件行號的查找實現,對照上下文就能實現,注意N_SLINE這就是之前說stab時提到的一個有用的屬性。

    // Search within [lline, rline] for the line number stab.
    // If found, set info->eip_line to the right line number.
    // If not found, return -1.
    //
    // Hint:
    //	There's a particular stabs type used for line numbers.
    //	Look at the STABS documentation and <inc/stab.h> to find
    //	which one.
    // Your code here.
    stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
    if(lline<=rline){
        info->eip_line=stabs[lline].n_desc;
    }else{
        return -1;
    }
    

    運行結果如下:

之后運行評分程序

至此,Lab1完結。完整代碼倉庫可點擊:https://github.com/Elio-yang/MIT6.828


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM