当前位置:网站首页>ucore lab1

ucore lab1

2022-06-13 00:13:00 Ethan97

practice 1: Understanding through make The process of generating an execution file

Question 1 : Operating system image file ucore.img How is it generated step by step ?( It needs to be explained in more detail Makefile The meaning of each relevant command and command parameter in , And explain the result of the command )

stay Makefile In the middle of ucore.img The code for is as follows :

# create ucore.img
UCOREIMG	:= $(call totarget,ucore.img)

$(UCOREIMG): $(kernel) $(bootblock)
	$(V)dd if=/dev/zero [email protected] count=10000
	$(V)dd if=$(bootblock) [email protected] conv=notrunc
	$(V)dd if=$(kernel) [email protected] seek=1 conv=notrunc

$(call create_target,ucore.img)

First, create a file with the size of 10000 Block of bytes , And then bootblock Copy the past .
Generate ucore.img You need to be successful kernel and bootblock

adopt make V= The specific commands to be executed are as follows :

#  compile  init.c  file , Generate  init.o 
+ cc kern/ern/init/init.c
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o

#  compile  readline.c  file , Generate  readline.o
+ cc kern/libs/readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o

#  compile  stdio.c  file , Generate  stdio.o
+ cc kern/libs/stdio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o

#  compile  kdebug.c  file , Generate  kdebug.o
+ cc kern/debug/kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o

#  compile  kmonitor.c  file , Generate  kmonitor.o
+ cc kern/debug/kmonitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o

#  compile  panic.c  file , Generate  painc.o
+ cc kern/debug/panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o

#  Compile a series  .c  file 
+ cc kern/driver/clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
+ cc kern/driver/intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
+ cc kern/trap/trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c
gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o

#  Link all the generated target files , And generate  kernel  Binary 
+ ld bin/kernel
ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/readline.o obj/kern/libs/stdio.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/debug/panic.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/intr.o obj/kern/driver/picirq.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/trap/vectors.o obj/kern/mm/pmm.o  obj/libs/printfmt.o obj/libs/string.o

#  compile  bootasm.S / bootmain.c / sign.c / 
+ cc boot/bootasm.S
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o

#  Generate  sign  file 
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

#  Link generation  bootblock  Binary 
+ ld bin/bootblock
ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!

#  Generate ucore.img file 
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0325965 s, 157 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.000118495 s, 4.3 MB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74923 bytes (75 kB) copied, 0.00055858 s, 134 MB/s

According to which we can see , To generate kernel, Need to use GCC Compiler will kern All the .c All files are compiled and generated .o File support . Again , To generate bootblock, First, you need to generate bootasm.o、bootmain.o、sign

GCC Compilation options Detailed explanation :

Compilation options meaning
-I Specify the path that the library file contains (① Specify the value ② environment variable ③ Standard system search path )
-fno-builtin Only identify with __builtin_ Prefixed GCC Built in functions , Disable most built-in functions , Prevent it from having the same name
-Wall After compilation, all Warning message
-ggdb Use GDB Join in Debugging information
-m32 Generate 32 Bit machine code ,int long pointer All are 32 position , Appoint x86 Processor specific options , Processor dependent options
-gstabs produce stabs Format debugging information , It doesn't contain GDB Expand
-nostdinc Do not search the header file of the standard system directory , Just search -I / -iquote / -isystem / -dirafter Specified header file , Catalog options
-fno-stack-protector Disable the stack protection mechanism , Tool options
-c Compile or assemble source files , But don't link . take .c/.i/.s And other suffixed files are compiled into .o suffix . Output type control
-O Optimize the generated code ,-Os Just optimize the size of the generated code , It turns on all -O2 Optimization options , Except for options that increase the size of the code . Optimization options

LD Compilation options Detailed explanation :

Compilation options meaning
-m Specify the format of the generated file , By default LDEMULATION environment variable , Without this environment variable , It depends on linker Default configuration . adopt ld -V You can view the emulation.
-nostdlib Search only the specified Library Directory displayed on the command line , The directory specified in the link script is ignored , Include link scripts developed on the command line .
-N Set up text and data section read-write , Data segments are not paged , Do not link dynamic link libraries
-e entry Specifies the entry function that the program starts executing , Instead of the default entry point .
-Tbss=org / -Tdata=org / -Ttext=org adopt org Draw up a section Absolute address in the output file .

dd Disk maintenance command Detailed explanation :

Linux dd The command is used to read 、 Convert and output data .dd Data can be read from standard input or file , Convert data according to the specified format , And then output to the file 、 Device or standard output .

if =  file name : Input file name , Default to standard input .
of =  file name : Output file name , Default to standard output .
	ibs = bytes: Read in at a time bytes Bytes , That is, to specify a block size of bytes Bytes .( Default  512  byte )
	obs = bytes: One output bytes Bytes , That is, to specify a block size of bytes Bytes .( Default  512  byte )
	bs = bytes: Also set read in / The output block size is bytes Bytes .
	cbs = bytes: One conversion bytes Bytes , That is, specify the conversion buffer size .
skip = blocks: Skip from the beginning of the input file blocks Block and start copying .
seek = blocks: Skip from the beginning of the output file blocks Block and start copying .
count = blocks: Just copy blocks Block , The size of the block is equal to ibs Number of bytes specified .
conv = < keyword >, Keywords can have the following 11 Kind of :
	conversion: Convert the file with the specified parameters .
	ascii: transformation ebcdic by ascii
	ebcdic: transformation ascii by ebcdic
	ibm: transformation ascii by alternate ebcdic
	block: Convert each line to a length of cbs, Fill in the insufficient part with space 
	unblock: Make each line the length of cbs, Fill in the insufficient part with space 
	lcase: Convert uppercase characters to lowercase characters 
	ucase: Convert lowercase characters to uppercase characters 
	swab: Exchange each pair of bytes of input 
	noerror: Don't stop when something goes wrong 
	notrunc: Do not truncate the output file 
	sync: Fill each input block to ibs Bytes , Empty the insufficient part (NUL) Character complement .

N and BYTES may be followed by the following multiplicative suffixes:
c =1, w =2, b =512, kB =1000, K =1024, MB =1000*1000, M =1024*1024, xM =M
GB =1000*1000*1000, G =1024*1024*1024, and so on for T, P, E, Z, Y.

Question two : What are the characteristics of a hard disk primary boot sector that the system considers to be compliant with the specification ?

answer : Of the primary boot sector of the hard disk size = 512 Bytes, And the last two bytes are 0x55AA.

stay sign.c The following checks were made :

    // Check the size of the primary boot sector 
		if (size != 512) {
    
        fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
        return -1;
    }

practice 2: Use qemu Execute and debug lab1 Software in .( Ask for a brief description of the exercise process in the report )

practice 3: analysis bootloader Process of entering protection mode .( It is required to write the analysis in the report )

bootloader Do the things :

1. Enable protection mode & Breaking mechanism

2. Read from hard disk kernel in ELF Format ucore kernel( With the MBR The following sectors ) And put it in a fixed position in memory

3. Jump to ucore Entrance point (entry point) perform , At this time, control is over ucore OS in

1. Why open A20, How to open A20?

Intel In the early 8086 CPU Provides 20 Root address line , The addressable space range is 02^20(00000HFFFFFH) Of 1MB Memory space . but 8086 Data processing bit width 16 position , No direct addressing 1MB Memory space , therefore 8086 An address translation mechanism of segment address plus offset address is provided .PC The addressing structure of the machine is segment:offset,segment and offset All are 16 Bit register , The maximum is 0ffffh, The calculation method of converting to physical address is to put segment Move left 4 position , Plus offset, therefore segment:offset The maximum address space that can be expressed should be 0ffff0h + 0ffffh = 10ffefh( Ahead 0ffffh yes segment=0ffffh And move to the left 4 The result of a , hinder 0ffffh Is the largest possible offset), This calculated 10ffefh How big ? It's about 1088KB, That is to say ,segment:offset The address of indicates capability , More than the 20 Physical addressing capability of bit address lines .** So when addressing to more than 1MB Memory time , It's going to happen “ Rewind ”( There will be no exceptions ). But the next generation is based on Intel 80286 CPU Of PC AT The computer system provides 24 Root address line , such CPU The addressing range of becomes 2^24=16M, It also provides a protection mode , Access to 1MB More memory , At this point, if you encounter “ Addressing more than 1MB” The situation of , The system will no longer “ Rewind ” 了 , This creates a downward incompatibility .** To maintain full downward compatibility ,IBM Decided to PC AT Add a hardware logic to the computer system , To imitate the above looping characteristics , And there it is A20 Gate. Their method is to put A20 Address line control and an output of the keyboard controller AND operation , To control A20 Opening of address line ( Can make ) On and off ( shielding \ prohibit ). At the beginning A20 Address line control is shielded ( Always for 0), Until the system software passes a certain IO Operation to open it ( see bootasm.S). Obviously , To access the high-end memory area in real mode , This switch must be on , In protected mode , Due to the use 32 Bit address line , If A20 Equal to 0, Then the system can only access odd megabytes of memory , That is, you can only access 0–1M、2-3M、4-5M…, This does not effectively access all available memory . So in protected mode , This switch must also be turned on .

2. How to initialize GDT surface ?

The sectioning machine involves 4 There are two key elements : Logical address 、 Segment descriptor ( Describe the properties of the segment )、 Segment descriptor table ( Containing multiple segment descriptors “ Array ”)、 Segment selector ( Segment register , The index used to locate the entry in the segment descriptor table )

Global descriptor table The global descriptor table is a table that holds multiple segment descriptors “ Array ”, The starting address is stored in the global descriptor table register GDTR in .GDTR Long 48 position , Among them high 32 Bit is the base address , low 16 Bit is the segment boundary . because GDT Can not have GDT Descriptors within itself are described and defined , So the processor uses GDTR by GDT This particular system segment . Be careful , The first segment descriptor in the global descriptor table is set as an empty segment descriptor .GDTR Segment boundaries in are in bytes . For containing N The segment bounds of descriptor tables of descriptors can usually be set to 8*N-1. stay ucore Medium boot/bootasm.S Medium gdt Address and kern/mm/pmm.c Array of global variables in gdt[] They are based on assembly language and C The concrete implementation of the global descriptor table of the language .

gdtdesc It is pointed out that the global descriptor table is in the symbol gdt It's about , take gdt Table load GDTR in .

#  hold gdt The starting position and bounds of the table are loaded GDTR register 
lgdt gdtdesc
#  take CR0 Of the 0 Location 1, To turn on the protection mode 
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

cr0 The first of the registers 0 Bit is the protection mode bit PE, Set up PE Will make the processor work in protected mode .

3. How to enter the protection mode ?

By long jump command ljmp $PROT_MODE_CSEG, $protcseg Enter protection mode .

make a concrete analysis bootmain.S The code is as follows :

bios Load from the first sector of the hard disk bootmain.S, And load it into the physical address of memory 0x7c00, Then start running in real mode .

#include <asm.h>

# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.set PROT_MODE_CSEG,        0x8                     # kernel code segment selector
.set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
.set CR0_PE_ON,             0x1                     # protected mode enable flag

# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16                                             # Assemble for 16-bit mode
    cli                                             # Disable interrupts
    cld                                             # String operations increment

    # Set up the important data segment registers (DS, ES, SS).  Set each register to 0
    xorw %ax, %ax                                   # Segment number zero
    movw %ax, %ds                                   # -> Data Segment
    movw %ax, %es                                   # -> Extra Segment
    movw %ax, %ss                                   # -> Stack Segment

    # Enable A20:
    #  For backwards compatibility with the earliest PCs, physical
    #  address line 20 is tied low, so that addresses higher than
    #  1MB wrap around to zero by default. This code undoes this.
    #   About A20 Gate: https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html
    #   Theoretically speaking , We just need to operate 8042 The output port of the chip (64h) Of bit 1, You can control A20 Gate,
    #   But actually , When you are ready to 8042 When writing data in the input buffer of , There may be other data that has not been processed ,
    #   therefore , We must first prohibit keyboard operation , At the same time, wait until there is no data in the data buffer , To really operate 8042 On or off A20 Gate.
    #   open A20 Gate The specific steps are as follows ( Reference resources bootasm.S):
    #   wait for 8042 Input buffer It's empty ;
    #   send out Write 8042 Output Port (P2) Order to 8042 Input buffer;
    #   wait for 8042 Input buffer It's empty ;
    #   take 8042 Output Port(P2) Get byte number 2 Location 1, And then write 8042 Input buffer;
seta20.1:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).  wait for 8042 The keyboard controller is not busy 
    testb $0x2, %al                                 #  Determine whether the input cache is empty 
    jnz seta20.1

    movb $0xd1, %al                                 # 0xd1 -> port 0x64,0xd1 Indicates the write output port command , The parameter is then passed 0x60 Port write 
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port

seta20.2:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.2

    movb $0xdf, %al                                 # 0xdf -> port 0x60, adopt 0x60 Write data 11011111  the A20 Set up 1
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

    # Switch from real to protected mode, using a bootstrap GDT
    # and segment translation that makes virtual addresses
    # identical to physical addresses, so that the
    # effective memory map does not change during the switch.
    #  load GDT surface 
    lgdt gdtdesc
    #  take CR0 Of the 0 Location 1, To turn on protected mode 
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

    # Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    #  Long jump to 32 Bit code snippet , reinstall CS and EIP
    ljmp $PROT_MODE_CSEG, $protcseg

.code32                                             # Assemble for 32-bit mode
protcseg:
    # Set up the protected-mode data segment registers
    #  Set up DS、ES Equal segment register 
    movw $PROT_MODE_DSEG, %ax                       # Our data segment selector
    movw %ax, %ds                                   # -> DS: Data Segment
    movw %ax, %es                                   # -> ES: Extra Segment
    movw %ax, %fs                                   # -> FS
    movw %ax, %gs                                   # -> GS
    movw %ax, %ss                                   # -> SS: Stack Segment

    # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
    #  Go to protected mode and finish , Get into boot Main method 
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

    # If bootmain returns (it shouldn't), loop.
spin:
    jmp spin

# Bootstrap GDT
.p2align 2                                          # force 4 byte alignment
gdt:
    SEG_NULLASM                                     # null seg
    SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel

gdtdesc:
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt

practice 4: analysis bootloader load ELF Format OS The process of .( It is required to write the analysis in the report )

By reading bootmain.c, understand bootloader How to load ELF file . By analyzing the source code and through qemu To run and debug bootloader&OS,

  • bootloader How to read hard disk sectors ?
  • bootmain The code for reading the hard disk is as follows :
unsigned int    SECTSIZE  =      512 ;
struct elfhdr * ELFHDR    =      ((struct elfhdr *)0x10000) ;     // scratch space
/* bootmain - the entry of bootloader */
void
bootmain(void) {
    
    // read the 1st page off disk
  	//  Read eight sectors into memory 
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
  	//  Check whether it is legal elf
    if (ELFHDR->e_magic != ELF_MAGIC) {
    
        goto bad;
    }
  	//do something else...
}

Based on the above bootmain Analysis of ,bootmain call readseg Function to read the hard disk sector , and readseg You call the readsect Read one sector at a time :

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

/*  The process of reading a sector ( For reference. boot/bootmain.c Medium readsect Function implementation ) As follows  1. Wait until the disk is ready  2. Issue the command to read the sector  3. Wait until the disk is ready  4. Read the disk sector data to the specified memory  */
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
    
    // wait for disk to be ready
    waitdisk();
    //  Write the address 0x1f2~0x1f5,0x1f7, Issue the command to read the disk 
    outb(0x1F2, 1);                         // count = 1
    outb(0x1F3, secno & 0xFF);
    outb(0x1F4, (secno >> 8) & 0xFF);
    outb(0x1F5, (secno >> 16) & 0xFF);
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

    // wait for disk to be ready
    waitdisk();

    // read a sector
  	//  Read a sector 
    insl(0x1F0, dst, SECTSIZE / 4);
}

/* * * readseg - read @count bytes at @offset from kernel into virtual address @va, * might copy more than asked. * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    
    uintptr_t end_va = va + count;

    // round down to sector boundary
    //  Move the starting address forward to the boundary 
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
    //  Calculate the first sector code to be read 
    uint32_t secno = (offset / SECTSIZE) + 1;

    //  Read sector by sector 
    for (; va < end_va; va += SECTSIZE, secno ++) {
    
        readsect((void *)va, secno);
    }
}
  • bootloader How to load ELF Format OS?

bootmain After reading the disk, start loading ELF file ,bootmain Load in ELF Format OS The code is as follows :

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    
  
    //read from disk

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    // ELF The head has a description ELF A description table of where the file should be loaded into memory , Read it out here and save it in ph
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    //  As described in the program header table , take ELF The data in the file is loaded into memory 
    for (; ph < eph; ph ++) {
    
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    //  according to ELF Entry information in the header table , Find the entry to the kernel and start running 
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

practice 5: Implement function call stack trace function ( You need to program )

Names of some registers

General Register( General registers ):EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP The low of these registers 16 Bit is 8086 Of AX/BX/CX/DX/SI/DI/SP/BP, about AX,BX,CX,DX These four registers , They can be accessed individually 8 Bit and low 8 position (AH,AL,BH,BL,CH,CL,DH,DL). Their meanings are as follows :

	EAX: accumulator 
	EBX: Base register 
	ECX: Counter 
	EDX: Data register 
	ESI: Source address pointer register 
	EDI: Destination address pointer register 
	EBP: Base pointer register 
	ESP: Stack pointer register 

Segment Register( Segment register , Also known as Segment Selector, Segment selector , Segment selector ): except 8086 Of 4 Out of segment (CS,DS,ES,SS),80386 Two paragraphs have also been added FS,GS, These segment registers are 16 Bit , Used for addressing memory segments with different attributes , Their meanings are as follows :

	CS: Code segment (Code Segment)
	DS: Data segment (Data Segment)
	ES: Additional data segments (Extra Segment)
	SS: stack segment (Stack Segment)
	FS: Additional segment 
	GS  Additional segment 

Instruction Pointer( Instruction pointer register ):EIP It's low 16 Bit is 8086 Of IP, It stores The memory address of the next instruction to be executed , In segmented address translation , Indicates the intra segment offset address of the instruction .

Understanding of stack

The two most important points to understand the call stack are : The structure of the stack ,EBP register (EBP: Base pointer register ) The role of . A function call action can be decomposed into : Zero to many PUSH Instructions ( Used to stack parameters ), One CALL Instructions .CALL In fact, the instruction also implies a return address ( namely CALL Instruction the address of the next instruction ) The action of pressing the stack ( It's done by hardware ,CALL The address of the next instruction of the instruction is the current eip Value ). Almost all local compilers insert assembly instructions like the following before each function body :

pushl   %ebp	#  take ebp Pressing stack 
movl   %esp , %ebp	# esp -> ebp, esp: Stack pointer register 

So before the program executes the actual instruction of a function , The following data has been put on the stack in sequence : Parameters 、 The return address 、ebp register . A stack structure similar to the following is obtained ( The order in which parameters are stacked depends on the calling method , Here we use C The language defaults to CDECL For example ):

+|   The direction at the bottom of the stack 		|  High address 
 |    ...		|
 |    ...	  |
 |   Parameters 3		|
 |   Parameters 2		|
 |   Parameters 1		|
 |   The return address 		|
 |   upper story [ebp]	| <-------- [ebp]
 |   local variable 		|   Low address 

The meaning of these two assembly instructions is : First of all, will ebp Register on stack , Then put the top of the stack pointer esp Assign a value to ebp.“mov ebp esp” On the surface, this instruction is written in esp Cover ebp The value of the original , It's not . Because to ebp Before assignment , primary ebp The value has been stacked ( Stack top ), And new ebp It just points to the top of the stack . here ebp Registers are already in a very important position , This register stores an address in the stack ( primary ebp The top of the stack after entering the stack ), From this address , Up ( The direction at the bottom of the stack ) Can get the return address 、 Parameter values , Down ( Stack top direction ) Can get the value of the local variable of the function , And the address stores the... When the function of the previous layer is called ebp value .

generally speaking ,ss:[ebp+4] Is the return address ,ss:[ebp+8] Is the first parameter value ( The parameter value of the last stack , It is assumed here that it occupies 4 Byte memory ),ss:[ebp-4] Is the first local variable ,ss:[ebp] This is the upper floor ebp value . because ebp The address in is always “ When the upper layer function is called ebp value ”, And in every layer of function calls , Can pass the ebp value “ Up ( The direction at the bottom of the stack )” Can get the return address 、 Parameter values ,“ Down ( Stack top direction )” Can get the value of the local variable of the function . Recursion is thus formed , Until you reach the bottom of the stack . This is the function call stack .

The change process of function call stack

Function calls generally include the following steps :

1、 Parameter stack : Turn the parameter from right to left ( Or from right to left ) Press into the system stack in turn .

2、 Return the address to the stack : Push the address of the next instruction of the current code area call instruction into the stack , For the function to continue when it returns .

3、 Code area jump : The processor jumps from the current code area to the entrance of the called function .

4、 Stack frame adjustment

4.1 Save the current stack frame state value , Ready for later recovery of this stack frame (EBP Push ).

4.2 Switch the current stack frame to the new stack frame ( take ESP Value loading EBP, Update the bottom of the stack frame ).

4.3 Allocate space for new stack frames ( hold ESP Subtract the size of the required space , Raise the top of the stack ).

Function return roughly includes the following steps :

1、 Save return value , Usually, the return value of the function is saved in the register EAX in .

2、 Pop up the current frame , Restore the previous stack frame .

2.1 On the basis of stack balance , to ESP Plus the size of the stack frame , Lower the top of the stack , Reclaim the space of the current stack frame

2.2 Save the previous stack frame saved at the bottom of the current stack frame EBP Value pop in EBP register , Restore the previous stack frame .

2.3 Pop the return address of the function to EIP register .

3、 Jump : Jump back to the parent function according to the return address of the function and continue to execute .

kern/debug/kdebug.c The comments and implementation of the are as follows :

/* * * print_stackframe - print a list of the saved eip values from the nested 'call' * instructions that led to the current point of execution *  Print a series of nested calls to the current execution location eip value  * * The x86 stack pointer, namely esp, points to the lowest location on the stack * that is currently in use. Everything below that location in stack is free. Pushing * a value onto the stack will involve decreasing the stack pointer and then writing * the value to the place that stack pointer points to. And popping a value does the * opposite. *  Stack pointer register esp Point to the lowest address of the stack currently in use , Below this address is the free space of the stack  *  Pressing the stack causes the stack pointer to point to a lower address , And write the value to the position pointed to by the stack pointer ; The stack operation is the opposite . * * The ebp (base pointer) register, in contrast, is associated with the stack * primarily by software convention. On entry to a C function, the function's * prologue code normally saves the previous function's base pointer by pushing * it onto the stack, and then copies the current esp value into ebp for the duration * of the function. If all the functions in a program obey this convention, * then at any given point during the program's execution, it is possible to trace * back through the stack by following the chain of saved ebp pointers and determining * exactly what nested sequence of function calls caused this particular point in the * program to be reached. This capability can be particularly useful, for example, * when a particular function causes an assert failure or panic because bad arguments * were passed to it, but you aren't sure who passed the bad arguments. A stack * backtrace lets you find the offending function. * ebp( Base register ) Use software to correlate with the stack , When entering a c Function time , * 1. The initialization code of a function usually converts the previous function's ebp Pop down to save  * 2. And then esp The value of ebp, To save function call information . * * The inline function read_ebp() can tell us the value of current ebp. And the * non-inline function read_eip() is useful, it can read the value of current eip, * since while calling this function, read_eip() can read the caller's eip from * stack easily. * read_ebp() Inform to get the current ebp Value ,read_eip() You can get the present eip Value  * * In print_debuginfo(), the function debuginfo_eip() can get enough information about * calling-chain. Finally print_stackframe() will trace and print them for debugging. * debuginfo_eip() You can get enough information about the function call chain ,print_stackframe() Will track 、 Print this information  * * Note that, the length of ebp-chain is limited. In boot/bootasm.S, before jumping * to the kernel entry, the value of ebp has been set to zero, that's the boundary. * ebp The length of the chain is limited , stay bootmain.S in ,ebp The value of is set to 0, This is its boundary . * */
void
print_stackframe(void) {
    
     /* LAB1 YOUR CODE : STEP 1 */
     /* (1) call read_ebp() to get the value of ebp. the type is (uint32_t); * (2) call read_eip() to get the value of eip. the type is (uint32_t); * (3) from 0 .. STACKFRAME_DEPTH * (3.1) printf value of ebp, eip * (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4] * (3.3) cprintf("\n"); * (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc. * (3.5) popup a calling stackframe * NOTICE: the calling funciton's return addr eip = ss:[ebp+4] * the calling funciton's ebp = ss:[ebp] */
    uint32_t ebp = read_ebp();  // (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
    uint32_t eip = read_eip();  // (2) call read_eip() to get the value of eip. the type is (uint32_t);

    int i, j;
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
    
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);   // (3.1) printf value of ebp, eip
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
    
            cprintf("0x%08x ", args[j]);   // (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]
        }
        cprintf("\n");  // (3.3) cprintf("\n");
        print_debuginfo(eip - 1);   // (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
        eip = ((uint32_t *)ebp)[1]; // eip go to caller Of CALL Instruction the address of the next instruction 
        ebp = ((uint32_t *)ebp)[0]; // (3.5) popup a calling stackframe  because ebp Pointing to caller's ebp The pointer to 
    }
}

practice 6: Improve interrupt initialization and processing ( You need to program )

Interrupt descriptor table ( It can also be referred to as interrupt vector table in protection mode ) How many bytes does a table item occupy in the ? Which of them represent the entry of interrupt processing code ?

An entry in the interrupt descriptor table accounts for 8 byte . among 0~ 15 Bit and 48~ 63 The bits are offset It's low 16 Position and height 16 position .16~ 31 Bit is segment selector . Obtain the base address of the segment through the segment selection sub segment , Add the offset in the segment to get the entry of the interrupt processing code .

Please perfect the programming kern/trap/trap.c The function that initializes the interrupt vector table in idt_init. stay idt_init Function , Initialize all interrupt entries in turn . Use mmu.h Medium SETGATE macro , fill idt An array of content . The entry of each interrupt is controlled by tools/vectors.c Generate , Use trap.c Declarative vectors The array can be .

About interrupt descriptor table idt: Interrupt number –> Interrupt descriptor table entry –> Interrupt handling routines

Each entry in the interrupt descriptor table records the address of an interrupt processing routine , Include segment selectors 、 The offset . Segment selectors can be selected in GDT Find segment descriptors in . Get the segment descriptor to get the base address of the interrupt service routine , add offset The starting address of the interrupt service routine is obtained .

The above search process is completed by hardware , But the tables are created by software , We just need lidt Give orders cpu Know the address of the interrupt descriptor table .

/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
    
     /* LAB1 YOUR CODE : STEP 2 */
     /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)? * All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ? * __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c * (try "make" command in lab1, then you will find vector.S in kern/trap DIR) * You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later. *  The entry address of the interrupt service routine is saved in __vectors in ,__vectors The array is in  kern/trap/vector.S *  take __vectors[] Declared as an external array of pointers  * * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT). * Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT * idt The array is the interrupt descriptor table , Using macros SETGATE To set up IDT Each of  * * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction. * You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more. * Notice: the argument of lidt is idt_pd. try to find it! *  Set it up IDT after , utilize lidt To make the cpu hear IDT The location of  */
    extern uintptr_t __vectors[];
    int i;
    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
    
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
   // set for switch from user to kernel
    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
   // load the IDT
    lidt(&idt_pd);
}

SETGATE The macro is located in kern/mm/mmu.h, The notes are as follows :

/* *
 * Set up a normal interrupt/trap gate descriptor
 *  Set interrupt / Trap gate descriptor 
 *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
 *   - sel: Code segment selector for interrupt/trap handler
 *   - off: Offset in code segment for interrupt/trap handler
 *   - dpl: Descriptor Privilege Level - the privilege level required
 *          for software to invoke this interrupt/trap gate explicitly
 *          using an int instruction.
 *   gate: For the corresponding idt[] An array of content , The entry address of the processing function 
 *   istrap: 1 yes trap door ,0 It's the interrupt door 
 *   sel:  interrupt / Code segment selector for trap gate processor 
 *   off:  The offset of the code segment 
 *   dpl:  The priority of the descriptor , The user program triggers this interrupt / The priority required by the trap 
 * */
#define SETGATE(gate, istrap, sel, off, dpl) {            \
    (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \
    (gate).gd_ss = (sel);                                \
    (gate).gd_args = 0;                                    \
    (gate).gd_rsv1 = 0;                                    \
    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
    (gate).gd_s = 0;                                    \
    (gate).gd_dpl = (dpl);                                \
    (gate).gd_p = 1;                                    \
    (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
}
Please perfect the programming trap.c Interrupt handling function in trap, Fill in... In the section dealing with clock interrupt trap The part of a function that handles clock interrupts , Make every time the operating system encounters 100 After the second clock interrupt , call print_ticks Subroutines , Print a line of text to the screen ”100 ticks”.

A clock is a peripheral with a special function , It's not just about timing . In the following chapters, we will talk about , It is precisely because of the regular clock interruptions , So that no matter what the present CPU Where to run , Operating systems are available at predetermined points in time CPU control power .

When you jump to the entry address of the interrupt processing routine , Such as :

vector2:
  pushl $0
  pushl $2
  jmp __alltraps
.globl vector3

It can be seen that the interrupt processing routine jumps to __alltraps,__alltraps be located kern/trap/trapentry.S in :

#include <memlayout.h>

# vectors.S sends all traps here.
.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame  structure trap frame structure 
    # therefore make the stack look like a struct trapframe  Make the stack look like a structure trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

    # load GD_KDATA into %ds and %es to set up data segments for kernel
    #  take GD_KDATA Load into ds and es Register to set the data segment for the kernel 
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

    # push %esp to pass a pointer to the trapframe as an argument to trap()
    #  take esp Press a stack to point a to trapframe The pointer to trap() function 
    pushl %esp

    # call trap(tf), where tf=%esp
    #  go to trap function 
    call trap

    # pop the pushed stack pointer
    popl %esp

    # return falls through to trapret...
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

The above assembly code finally calls kern/trap/trap.c Medium trap function :

/* * * trap - handles or dispatches an exception/interrupt. if and when trap() returns, * the code in kern/trap/trapentry.S restores the old CPU state saved in the * trapframe and then uses the iret instruction to return from the exception. * trap Function processing 、 Distribution interrupted / abnormal , *  When trap When function returns ,trapentry.S Restore saved in trapframe The original in cpu state , *  And then use iret Command to return an exception . * */
void
trap(struct trapframe *tf) {
    
    // dispatch based on what type of trap occurred
    trap_dispatch(tf);
}

among trapframe The structure is defined as follows :

/* registers as pushed by pushal */
struct pushregs {
    
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;            /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    //  The following information is automatically saved by the hardware 
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    //  The following is the information that needs to be saved for the user state to generate an interrupt 
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

trap_dispatch The implementation is as follows :

/* trap_dispatch - dispatch based on what type of trap occurred */
static void
trap_dispatch(struct trapframe *tf) {
    
    char c;

    switch (tf->tf_trapno) {
    
    case IRQ_OFFSET + IRQ_TIMER:	//  Clock interrupt 
        /* LAB1 YOUR CODE : STEP 3 */
        /* handle the timer interrupt */
        //  Dealing with clock interrupts 
        /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c *  After a clock interrupt , Should let ticks The variable is incremented to record it  * (2) Every TICK_NUM cycle, you can print some info using a function, such as print_ticks(). *  Each pass TICK_NUM Secondary clock interrupt , Should use the print_ticks() Print some information  * (3) Too Simple? Yes, I think so! */
        ticks ++;	//  Each clock interrupt counter is incremented by one 
        if (ticks % TICK_NUM == 0) {
    
            print_ticks();  //  Every time TICK_NUM The clock interrupt calls once 
        }
        break;
    }
}
原网站

版权声明
本文为[Ethan97]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202280602093910.html