练习一
Makefile简单分析
make V=
显示详细的命令执行过程。
生成内核
添加编译参数等旗标
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
生成.o文件
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
生成kernel的实际代码
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
将@符号放到命令行前,将不显示这个命令,只会默默执行这个命令即“+ ld bin/kernel”
@echo + ld $@
注意这里链接过程,里面的“-T tools/kernel.ld”和实际的对应即
“ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o”
该命令结束时,生成内核
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
只会默默执行这两条命令
反汇编
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
打印符号表
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
+ cc kern/init/init.c
+ cc kern/libs/stdio.c
+ cc kern/libs/readline.c
+ cc kern/debug/panic.c
+ cc kern/debug/kdebug.c
+ cc kern/debug/kmonitor.c
+ cc kern/driver/clock.c
+ cc kern/driver/console.c
+ cc kern/driver/picirq.c
+ cc kern/driver/intr.c
+ cc kern/trap/trap.c
+ cc kern/trap/vectors.S
+ cc kern/trap/trapentry.S
+ cc kern/mm/pmm.c
+ cc libs/string.c
+ cc libs/printfmt.c
+ ld bin/kernel
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o
接着还会执行objdump的两条命令,但并不显示出来
生成bootloader
编译sign相关
add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))
create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))
...
# create 'sign' tools
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
编译bootloader相关
编译源文件为目标文件
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
获取目标文件
bootblock = $(call totarget,bootblock)
生成bootloader
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
“+ ld bin/bootblock”
@echo + ld $@
此时生成bootloader即bootblock.o,末尾不带0x55aa
“ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o”
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJDUMP) -t $(call objfile,bootblock) | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,bootblock)
拷贝二进制代码bootblock.o到bootblock.out,这么做是为了去掉bootblock.o里面
用于gdb调试的符号信息,从而减小最终的目标文件大小,以便小于510B!!!
“objcopy -S -O binary obj/bootblock.o obj/bootblock.out”
-S 移除所有符号和重定位信息
-O <bfdname> 指定输出格式
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
使用sign工具处理bootblock.out,生成bootblock,末尾带着0x55aa
“bin/sign obj/bootblock.out bin/bootblock”
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
+ 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
接着还会执行objdump/objcopy/sign相关命令,但不显示出来
sign工具
处理bootblock.out,生成bootblock。该工具首先检查生成的bootloader在经过objcopy处理之后是否在510B以内,超过这个尺寸会报错;接着为其末尾添加0x55AA“扇区结束使用”的标记,使其之后可以作为合法的一个硬盘扇区的内容。注意,这里生成的bootloader只有500B,而0x55AA却写在了扇区结束位置,中间还空了几个字节,这么做是没有问题的,因为BIOS只会检查扇区是不是合法,如果合法,就将控制权转让给bootloader。当bootloader拥有控制权开始掌控一切,执行到第五百个字节附近的命令时,就会继续将控制权转让给内核,接下来就是内核的事情了,这10B可以认为是浪费了。
printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
'obj/bootblock.out' size: 500 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
生成最终的映像文件(相当于一个搭载操作系统的硬盘)
# create ucore.img
UCOREIMG := $(call totarget,ucore.img)
$(UCOREIMG): $(kernel) $(bootblock)
生成一个有10000个块的文件,每个块默认512字节,用0填充
$(V)dd if=/dev/zero of=$@ count=10000
把bootblock中的内容写到第一个块
$(V)dd if=$(bootblock) of=$@ conv=notrunc
从第二个块开始写kernel中的内容
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0203533 s, 252 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000162587 s, 3.1 MB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74880 bytes (75 kB, 73 KiB) copied, 0.000266092 s, 281 MB/s
练习二
make debug
debug: $(UCOREIMG)
$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &
$(V)sleep 2
gnome-terminal
$(V)$(TERMINAL) -e "cgdb -q -x tools/gdbinit"
qemu-system-i386命令相关参数
- -S:Do not start CPU at startup (you must type 'c' in the monitor)
- -s:Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234.快速连接gdb。
- -parallel dev:Redirect the virtual parallel port to host device dev (same devices as the serial port). On Linux hosts, /dev/parportN can be used to use hardware devices connected on the corresponding host parallel port. stdio表示[Unix only] standard input/output
- -hda file:hda/b/c/d,Use file as hard disk 0, 1, 2 or 3 image.本例中,以ucore.image文件作为虚拟机的硬盘。
- -serial dev:Redirect the virtual serial port to host character device dev. The default device is "vc" in graphical mode and "stdio" in non graphical mode. null 表示void device。
gnome-terminal(打开一个新的终端)命令相关参数
-e:Execute the argument to this option inside the terminal.
-x:Execute the remainder of the command line inside the terminal.
cgdb
gdb的终端界面增强版,基于gdb的命令行可视化工具,用来替代gdb -tui,可以像原来使用gdb一样去使用它,cgdb实际上就是在gdb的基础上套了一层交互,能在终端里运行,便于你边调试边看代码。
-q:Do not print version number on startup.不打印版本信息
-x:从文件中执行gdb命令
单步跟踪BIOS
查看各个寄存器的内容,确认此时cs:ip=0xf000:0xfff0
info reg
查看指令指针寄存器的内容,以便确定当前要执行的指令在哪里,注意,此时已经开始使用pc=cs*16+ip
p/x $pc
p/x $cs
查看0xffff0内存地址下的二进制值
x /8w 0xffff0
查看这些值作为指令的内容,此时已经开始使用pc=cs*16+ip,所以执行的是0xffff0地址处的指令
这里的指令为ljmp $0xf000,$0xe05b
x /8i 0xffff0
如果使用该命令查看,会发下此时的指令全部是add,对应的值全部是0,这是因为这个命令此时查的
地址其实是0x0000fff0而不是cs:ip=0xf000:0xfff0,注意,x命令只会机械的对应内存地址,
不会进行pc=cs*16+ip计算,而且也不应该进行这个计算,x就是查内存地址的!!!
x /8i $pc 等效于 x /8i 0x0000fff0
x /8w $pc
单步执行一条指令
si
查看pc寄存器的值为0xe05b(同eip)
p/x $pc
查看cs寄存器的值为0xf000
p/x $cs
查看0xfe05b位置(即cs*16+ip)处的指令,这就是接下来要继续执行的指令cmpl $0x0,%cs:0x6c48x /8i 0xfe05b
(gdb) x /8i 0xfe05b
0xfe05b: cmpl $0x0,%cs:0x6c48
0xfe062: jne 0xfd2e1
0xfe066: xor %dx,%dx
0xfe068: mov %dx,%ss
0xfe06a: mov $0x7000,%esp
0xfe070: mov $0xf3691,%edx
0xfe076: jmp 0xfd165
0xfe079: push %ebp
(gdb) si
0x0000e062 in ?? ()
(gdb) p/x $pc
$16 = 0xe062
(gdb) p/x $cs
$17 = 0xf000
(gdb) x /8i 0xfe062
0xfe062: jne 0xfd2e1
0xfe066: xor %dx,%dx
0xfe068: mov %dx,%ss
0xfe06a: mov $0x7000,%esp
0xfe070: mov $0xf3691,%edx
0xfe076: jmp 0xfd165
0xfe079: push %ebp
0xfe07b: push %edi
注意,答案中说使用x /2i $pc
来查看BIOS代码是错误的,这个根本看不了代码,这个看的仅仅是pc
寄存器所保存的数值作为内存地址时,该地址下的数据什么,可以看到全部是0!!!事实上,pc
配合cs
得到的地址才是真正的指令在内存的地址,所以真正想找到指令地址,必须先手动通过pc=cs*16+ip
计算出指令在内存中的地址,然后再用x /8i 地址
来找到对应的指令!!!另外,有一个现象,当cs的值变为0x8的时候,eip此时已经是20位地址,不再使用pc=cs*16+ip
计算指令地址,而是直接使用eip内的数据作为指令地址。
如图所示,CPU上电执行的第一条指令的实际地址既是0xffff0也是0xfffffff0!!!这两个地址下的命令貌似是同一条,都是ljmp这条长跳转指令,BIOS代码真正的执行位置在0xfe05b,BIOS执行的第一条汇编指令是cmpl
!!!
设置bootloader断点和kernel断点
由于是直接连接qemu进行调试,刚开始运行的是BIOS的代码,这里面没有符号表,只能根据地址去打断点,不能用函数名去打断点,而且就算有符号表,也是人家BIOS的符号表,不是bootloader和kernel的,所以要用file命令专门把bootloader和kernel各自的可执行文件分别读入gdb。ucore.img只是个映像文件,里面是两个可执行文件,但ucore.img本身并不是可执行文件,所以不能把ucore.img读入gdb。bootloader和kernel都在ucore.img中,但本质是两个不同的可执行文件,所以要分别将其读入gdb才行,对bootloader的函数打断点的时候,只用file读入bootloader的可执行文件;对kernel的函数打断点的时候,只用file读入kernel的可执行文件。
bootloader包含符号表和可调试信息的可执行文件是obj/bootblock.o(从makefile里面的编译信息里看出来的,找到带着-ggdb -stabs
选项的最终的“可执行文件”注意不单单是“目标文件”!!!),而obj/bootblock.out是obj/bootblock.o经过objcopy处理,将可调试信息(例如符号表)等去掉之后的可执行文件,比obj/bootblock.o小,所以才要把obj/bootblock.out放到ucore.img以保证尺寸小于510B而不是直接把obj/bootblock.o放到ucore.img!!!
kernel的包含符号表和可调试信息的可执行文件就是bin/kernel,从makefile里面的编译信息看出来的。
要想让最终的可执行文件包含可被gdb调试的信息,在编译目标文件的时候,就要加上相关的参数-ggdb -gstabs
注意这里的-c,编译的是目标文件,没链接呢,只是用来判断带着调试信息的可执行文件从哪里来
最终给file命令使用的得是经过ld链接之后的可执行文件
gcc -Ikern/init/ -fno-builtin -fno-PIC -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
gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
注意-g -O2
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
(gdb) file obj/bootblock.o
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
A program is being debugged already.
Reading symbols from obj/bootblock.o...done.
(gdb) b bootmain
Breakpoint 1 at 0x7d0f: file boot/bootmain.c, line 95.
(gdb) continue
Continuing.
Breakpoint 1, bootmain () at boot/bootmain.c:95
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00007d0f in bootmain at boot/bootmain.c:95
breakpoint already hit 1 time
(gdb) d 1
(gdb) info break
No breakpoints or watchpoints.
-----------------------------------------------------
(gdb) file bin/kernel
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
A program is being debugged already.
Load new symbol table from "bin/kernel"? (y or n) y
Reading symbols from bin/kernel...done.
(gdb) b kern_init
Breakpoint 2 at 0x100000: file kern/init/init.c, line 25.
(gdb) b pmm_init
Breakpoint 3 at 0x102a7d: file kern/mm/pmm.c, line 96.
(gdb) continue
Continuing.
Breakpoint 2, kern_init () at kern/init/init.c:25
(gdb) continue
Continuing.
Breakpoint 3, pmm_init () at kern/mm/pmm.c:96
(gdb) continue
...
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x00100000 in kern_init at kern/init/init.c:25
breakpoint already hit 1 time
3 breakpoint keep y 0x00102a7d in pmm_init at kern/mm/pmm.c:96
breakpoint already hit 1 time
(gdb) d 2
(gdb) d 3
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) q
练习三、四
bootloader
BIOS读取硬盘主引导扇区到内存从而把bootloader的可执行文件从硬盘主引导扇区读入内存0x7c00位置,并转跳到0x7c00开始执行bootloader。在ucore中,bootloader涉及的代码文件为asm.h、bootasm.S、bootmain.c。
“bootloader”完成的工作包括:
- 切换到保护模式,启用分段机制
- 读磁盘中ELF执行文件格式的ucore操作系统到内存
- 显示字符串信息
- 把控制权交给ucore操作系统
- 对应其工作的实现文件在lab1中的boot目录下的三个文件asm.h、bootasm.S和bootmain.c。
$ objdump -x obj/bootblock.o
obj/bootblock.o: file format elf32-i386
obj/bootblock.o
architecture: i386, flags 0x00000012:
EXEC_P, HAS_SYMS
start address 0x00007c00 ------------------注意这里,bootloader的起始地址
Program Header:
LOAD off 0x00000074 vaddr 0x00007c00 paddr 0x00007c00 align 2**2
filesz 0x000001f4 memsz 0x000001f4 flags rwx
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
filesz 0x00000000 memsz 0x00000000 flags rwx
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000184 00007c00 00007c00 00000074 2**2
CONTENTS, ALLOC, LOAD, CODE
1 .eh_frame 00000068 00007d84 00007d84 000001f8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000008 00007dec 00007dec 00000260 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .stab 0000063c 00000000 00000000 00000268 2**2
CONTENTS, READONLY, DEBUGGING
4 .stabstr 0000074b 00000000 00000000 000008a4 2**0
CONTENTS, READONLY, DEBUGGING
5 .comment 00000035 00000000 00000000 00000fef 2**0
CONTENTS, READONLY
SYMBOL TABLE:
00007c00 l d .text 00000000 .text
00007d84 l d .eh_frame 00000000 .eh_frame
00007dec l d .data 00000000 .data
00000000 l d .stab 00000000 .stab
00000000 l d .stabstr 00000000 .stabstr
00000000 l d .comment 00000000 .comment
00000000 l df *ABS* 00000000 bootmain.c
00007c72 l F .text 0000009d readseg
00000000 l df *ABS* 00000000 obj/boot/bootasm.o
00000008 l *ABS* 00000000 PROT_MODE_CSEG----常量
00000010 l *ABS* 00000000 PROT_MODE_DSEG----常量
00000001 l *ABS* 00000000 CR0_PE_ON----常量
00007c0a l .text 00000000 seta20.1-----汇编函数的起始地址
00007c14 l .text 00000000 seta20.2-----汇编函数的起始地址
00007c6c l .text 00000000 gdtdesc
00007c32 l .text 00000000 protcseg
00007c4f l .text 00000000 spin
00007c54 l .text 00000000 gdt-------注意GDT的起始地址
00007df0 g O .data 00000004 SECTSIZE
00007dec g O .data 00000004 ELFHDR
00007d0f g F .text 00000075 bootmain----和使用gdb的结果相同,注意该函数的起始地址
00007df4 g .data 00000000 __bss_start
00007df4 g .data 00000000 _edata
00007df4 g .data 00000000 _end
00007c00 g .text 00000000 start
bootasm.S
BIOS代码:
...
IN:
0x000f1eb6: movw $0x200,0x32(%esp)
0x000f1ebd: mov %ebx,0x2e(%esp)
0x000f1ec1: mov %esi,%eax
0x000f1ec3: mov %al,0x22(%esp)
0x000f1ec7: movw $0xaa55,0x2a(%esp)
0x000f1ece: mov $0xf9135,%ecx
0x000f1ed3: xor %edx,%edx
0x000f1ed5: lea 0xe(%esp),%eax
0x000f1ed9: call 0xf0abb
上面的全部都是BIOS代码,从这里开始进入bootloader,下面这几句就是bootloader开
始的几句代码
IN:
0x00007c00: cli
0x00007c01: cld
0x00007c02: xor %ax,%ax
0x00007c04: mov %ax,%ds
0x00007c06: mov %ax,%es
0x00007c08: mov %ax,%ss
...
对比bootasm.S中的代码,可以看出bootasm.S中的代码才是bootloader入口的代码,当然,
bootloader的入口也可以从链接命令的“-e start”看出来
# 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).
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
...
按序干的事情:
- 禁用中断
- 设置字符串从前往后处理
- 将段寄存器DS/ES/SS全部置零
- 使能A20,这样就可以寻址整个 80286 的 16M 内存,或者是寻址 80386级别机器的所有 4G 内存。
seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty). Read a byte from 0x64 port to al testb $0x2, %al # Test the 2nd(从0开始的第一位,0b00000010) bit of al # 不等于0则跳回去继续循环 jnz seta20.1 # if the 2nd bit of al == 0, jump out the cycle等于0则跳出循环! # 写Output Port:向64h发送0d1h命令,然后向60h写入Output Port的数据 movb $0xd1, %al # 0xd1 -> port 0x64 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 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
- 将GDT的关键信息(大小和起始地址)加载到GDTR寄存器
# load GDT into register GDTR, # including the size and the originating address of GDT lgdt gdtdesc ... ... ... 关于GDT: # Bootstrap GDT .p2align 2 # force 4 byte alignment四字节对齐 gdt: # GDT的第一个描述符是全0,空段描述符 SEG_NULLASM # null seg # 设置代码段,可读可执行,起始地址为0x0,大小为4G SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel # 设置数据段,可写,起始地址为0x0,大小为4G SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel # 从这里可以看出,GDTR要的数据就两个,而且还是有顺序的,第一个是GDT的大小,第二个是GDT的起始地址 # GDT的起始地址就是在汇编代码中的段标号gdt gdtdesc: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
- CR0置位,CPU从实模式转换到保护模式
movl %cr0, %eax # I guess the value in cr0 is set when cpu is started. orl $CR0_PE_ON, %eax # set the 0 bit of eax 1 movl %eax, %cr0 # now, start the protected mode
- 长跳转到32位模式的代码段(32位代码段,也是个汇编函数入口)“protcseg”,设置保护模式下的数据段寄存器。
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector .set PROT_MODE_DSEG, 0x10 # kernel data segment selector ... .code16 ... seta20.2: ... # 长跳转指令ljmp会修改CS:IP的直,这里CS的直会被修改为$PROT_MODE_CSEG即0x8 # 这就是为什么后面使用GDB调试时发现CS直变为了0x8了!!!就是在这里被修改的!!! ljmp $PROT_MODE_CSEG, $protcseg .code32 # Assemble for 32-bit mode protcseg: # Set up the protected-mode data segment registers 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
- 设置栈指针(建立堆栈)并调用C函数bootmain
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00) movl $0x0, %ebp movl $start, %esp call bootmain
这里将栈的范围设置为0~0x7c00,并且调用C函数bootmain来进行接下来的动作。理论上,bootmain函数是不应该返回的!!!
bootmain.c
0.分析kernel的elf文件格式
参考《程序员的自我修养》里面关于elf文件格式的内容来分析,见6.4节的内容。
使用hd bin/kernel > hd_kernel_file
来查看对应的二进制文件
注意使用小端读法
6 #define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endia| 6 00000040 00 00 10 00 1e d9 00 00 1e d9 00 00 05 00 00 00 |..........
7 | 7 00000050 00 10 00 00 01 00 00 00 00 f0 00 00 00 e0 10 00 |..........
8 /* file header */ | 8 00000060 00 e0 10 00 16 0a 00 00 80 1d 00 00 06 00 00 00 |..........
9 struct elfhdr { | 9 00000070 00 10 00 00 51 e5 74 64 00 00 00 00 00 00 00 00 |....Q.td..
10 uint32_t e_magic; // must equal ELF_MAGIC | 10 00000080 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 |..........
11 uint8_t e_elf[12]; | 11 00000090 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |..........
12 uint16_t e_type; // 1=relocatable, 2=executable, 3=shared objec| 12 000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |..........
13 uint16_t e_machine; // 3=x86, 4=68K, etc. | 13 *
14 uint32_t e_version; // file version, always 1 | 14 00001000 55 89 e5 83 ec 18 ba 80 fd 10 00 b8 16 ea 10 00 |U.........
15 uint32_t e_entry; // entry point if executable | 15 00001010 29 c2 89 d0 83 ec 04 50 6a 00 68 16 ea 10 00 e8 |)......Pj.
16 uint32_t e_phoff; // file position of program header or 0 | 16 00001020 95 2d 00 00 83 c4 10 e8 42 15 00 00 c7 45 f4 60 |.-......B.
17 uint32_t e_shoff; // file position of section header or 0 | 17 00001030 35 10 00 83 ec 08 ff 75 f4 68 7c 35 10 00 e8 0a |5......u.h
18 uint32_t e_flags; // architecture-specific flags, usually 0 | 18 00001040 02 00 00 83 c4 10 e8 a1 08 00 00 e8 79 00 00 00 |..........
19 uint16_t e_ehsize; // size of this elf header | 19 00001050 e8 28 2a 00 00 e8 57 16 00 00 e8 b8 17 00 00 e8 |.(*...W...
20 uint16_t e_phentsize; // size of an entry in program header | 20 00001060 ef 0c 00 00 e8 85 17 00 00 e8 50 01 00 00 eb fe |..........
21 uint16_t e_phnum; // number of entries in program header or 0 | 21 00001070 55 89 e5 83 ec 08 83 ec 04 6a 00 6a 00 6a 00 e8 |U........j
22 uint16_t e_shentsize; // size of an entry in section header | 22 00001080 bd 0c 00 00 83 c4 10 90 c9 c3 55 89 e5 53 83 ec |..........
23 uint16_t e_shnum; // number of entries in section header or 0 | 23 00001090 04 8d 4d 0c 8b 55 0c 8d 5d 08 8b 45 08 51 52 53 |..M..U..].
24 uint16_t e_shstrndx; // section number that contains section name s| 24 000010a0 50 e8 ca ff ff ff 83 c4 10 90 8b 5d fc c9 c3 55 |P.........
25 }; | 25 000010b0 89 e5 83 ec 08 83 ec 08 ff 75 10 ff 75 08 e8 c7 |.........u
26 | 26 000010c0 ff ff ff 83 c4 10 90 c9 c3 55 89 e5 83 ec 08 b8 |.........U
27 /* program section header */ | 27 000010d0 00 00 10 00 83 ec 04 68 00 00 ff ff 50 6a 00 e8 |.......h..
28 struct proghdr { | 28 000010e0 cb ff ff ff 83 c4 10 90 c9 c3 55 89 e5 83 ec 18 |..........
29 uint32_t p_type; // loadable code or data, dynamic linking info,et| 29 000010f0 8c 4d f6 8c 5d f4 8c 45 f2 8c 55 f0 0f b7 45 f6 |.M..]..E..
30 uint32_t p_offset; // file offset of segment | 30 00001100 0f b7 c0 83 e0 03 89 c2 a1 20 ea 10 00 83 ec 04 |.........
31 uint32_t p_va; // virtual address to map segment | 31 00001110 52 50 68 81 35 10 00 e8 31 01 00 00 83 c4 10 0f |RPh.5...1.
32 uint32_t p_pa; // physical address, not used | 32 00001120 b7 45 f6 0f b7 d0 a1 20 ea 10 00 83 ec 04 52 50 |.E..... ..
33 uint32_t p_filesz; // size of segment in file | 33 00001130 68 8f 35 10 00 e8 13 01 00 00 83 c4 10 0f b7 45 |h.5.......
34 uint32_t p_memsz; // size of segment in memory (bigger if contains | 34 00001140 f4 0f b7 d0 a1 20 ea 10 00 83 ec 04 52 50 68 9d |..... ....
35 uint32_t p_flags; // read/write/execute bits | 35 00001150 35 10 00 e8 f5 00 00 00 83 c4 10 0f b7 45 f2 0f |5.........
36 uint32_t p_align; // required alignment, invariably hardware page s| 36 00001160 b7 d0 a1 20 ea 10 00 83 ec 04 52 50 68 ab 35 10 |... ......
37 }; | 37 00001170 00 e8 d7 00 00 00 83 c4 10 0f b7 45 f0 0f b7 d0 |..........
elfhdr:
e_type = 2
e_machine = 3
e_version = 1
e_entry = 0x100000
e_phoff = 0x34 program header在该文件中的偏移量为0x34B,52B
e_shoff = 0x0122c8
e_ehsize = 0x34 elf文件头的大小为0x34B,52B
e_phentsize = 0x20 i.e.32B
e_phnum = 0x3
e_shentsize = 0x28
e_shnum = 0x0b 有11个section,但是首个section是空的,所以共有10个有效的section(回顾段描述符)
e_shstrndx = 0x08
由于e_phnum = 0x3,所以应该是个结构体数组,每个元素32B
对照二进制分析的结果 | proghdr 1 | proghdr 2 | proghdr 3 |
p_type | 0x01 | 0x01 | 0x6474e551 |
p_offset | 0x1000 | 0xf000 | 0 |
p_va | 0x100000 | 0x10e000 | 0 |
p_pa | 0x100000 | 0x10e000 | 0 |
p_filesz | 0xd91e | 0x0a16 | 0 |
p_memsz | 0xd91e | 0x1d80 | 0 |
p_flags | 0x05 | 0x06 | 0x07 |
p_align | 0x1000 | 0x1000 | 0x10 |
结合《程序员的自我修养》所述,“.text/.rodata/.data/.bss/等”都是section,几个section会组成一个segment,加载elf文件的时候是以segment为单位来加载的,“program header”用来描述segment。从上图可以看出,偏移量为0x001000的segment由“.text .rodata .stab .stabstr”这几个section组成,偏移量为0x00f000的segment由“.data .bss”这几个section组成。还可以看出,kernel的elf文件的偏移量为0x1000处就是内核代码开始的地方,也就是说,kern_init()在该elf文件的0x1000处,要被加载到内存的0x100000处!!!同理,内核的数据在elf文件的0xf000处,要被加载到内存的0x10e000处。
1.将前8个扇区(4KB,0x1000KB)的kernel(共74KB)装载到内存,用于解析kernel可执行文件的elf文件头、分析program header表中有几项(即该elf文件中有几个segment需要被加载到内存以及如何加载到内存)。这8个扇区包含了elf文件头和program header table以及一些没用的信息。
# ELFHDR指向0x10000处,而kern_init()在0x100000处,bootloader是将连续的几个扇区一口气
# 都读到内存中,而kernel又是一个完整的elf文件,从下面的代码可以看出,kernel这个完整
# 的elf文件的前4KB是被完整的放到了内存0x10000处,但这里并不是kern_init()的地址,说明从0x10000
# 开始到0x10000+4KB的这段内存空间用于放置elf文件头以及接下来的内容了,这些内容用于分析
# 整个elf文件并将有效的segment加载到内存。
# elf中的内容并不是都要载入内存的,分析program header将有效的segment载入内存即可。
# 感觉用不着读8个扇区,有点多了,只要把elf文件头和program header表全部读出来并解析,
# 剩下的就是从磁盘往出读具体的segment到ph->va所指向的内存地址了。
34 /* 0x10000 = 64KB */
35 struct elfhdr * ELFHDR = ((struct elfhdr *)0x10000) ; // scratch space
90 /* bootmain - the entry of bootloader */
91 void
92 bootmain(void) {
93 // read the 1st page off disk
94 /* 一口气读入8个扇区,其中还包括存放bootloader 代码的扇区 */
# 整个kernel文件74K,但是8个扇区只有4K
95 readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
96
97 // is this a valid ELF?
# elf文件头只有52B,一个扇区就包含了,这里就已经把elf文件头解析出来了!!!
98 if (ELFHDR->e_magic != ELF_MAGIC) {
99 goto bad;
100 }
101
102 struct proghdr *ph, *eph;
103
104 // load each program segment (ignores ph flags)
# 解析出来第一个program header,从ELFHDR->e_phnum是3可知一共
# 有3个segment还在硬盘中等待载入内存
108 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
109 eph = ph + ELFHDR->e_phnum;
......
129 }
65 /* *
66 * readseg - read @count bytes at @offset from kernel into virtual address @va,
67 * might copy more than asked.
68 * */
69 static void
70 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
71 uintptr_t end_va = va + count;
72
73 // round down to sector boundary
74 va -= offset % SECTSIZE;
75
76 // translate from bytes to sectors; kernel starts at sector 1
77 /* 读入的首个扇区存的是bootloader,所以这里要
78 * 往后偏一个扇区才是放内核代码的扇区
79 */
# 0扇区给bootloader
# 1-8扇区是这次读进来的
80 uint32_t secno = (offset / SECTSIZE) + 1;
81
82 // If this is too slow, we could read lots of sectors at a time.
83 // We'd write more to memory than asked, but it doesn't matter --
84 // we load in increasing order.
85 for (; va < end_va; va += SECTSIZE, secno ++) {
86 readsect((void *)va, secno);
87 }
88 }
# secno 表示具体第几个扇区
44 /* readsect - read a single sector at @secno into @dst */
45 /* https://blog.csdn.net/ml_1995/article/details/51044260 内联汇编基础语法 */
46 static void
47 readsect(void *dst, uint32_t secno) {
48 // wait for disk to be ready
49 waitdisk();
50
# 以下都是在告诉硬盘IO接下来要读取哪个扇区了
# 这里的out都是在让IO做准备!!!在设置IO要去哪个扇区做读取准备
# 说白了就是让磁头对准指定扇区!!!
51 outb(0x1F2, 1); // count = 1 只读取一个扇区
52 outb(0x1F3, secno & 0xFF); /* 要读取扇区的编号 */
53 outb(0x1F4, (secno >> 8) & 0xFF); /* 用来存放读写柱面的低8位字节 */
54 outb(0x1F5, (secno >> 16) & 0xFF); /* 用来存放读写柱面的高2位字节 */
55 outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); /* 用来存放要读/写的磁盘号及磁头号 */
// 上面四条指令联合制定了扇区号
// 在这4个字节线联合构成的32位参数中
// 29-31位强制设为1
// 28位(=0)表示访问"Disk 0"
// 0-27位是28位的偏移量
# 通知硬盘IO,要读取扇区了,但这里并未真的开始读取扇区
56 outb(0x1F7, 0x20); // cmd 0x20 - read sectors
57
58 // wait for disk to be ready
59 waitdisk();
60
61 // read a sector
# 实际开始从指定扇区读取指定量的数据到指定内存
# 去哪个扇区读已经在上面的outb代码中设置好了
# 这里只要设置“开始读取”的行为(0x1F0)、读多少以及读到哪里就行
62 insl(0x1F0, dst, SECTSIZE / 4); /* insl 是双字输入,所以这里除以4 */
63 }
# 0x40表示磁盘已经准备好了
37 /* waitdisk - wait for disk ready */
38 static void
39 waitdisk(void) {
40 while ((inb(0x1F7) & 0xC0) != 0x40)
41 /* do nothing */;
42 }
# "inb a b"表示IO操作,从a读取一个字节到b
# 内联汇编:%0和%1都是让编译器自己从CPU的通用寄存器中选两个可用的出来用这两个数指代
# "=a"(data)表示输出到eax相关的寄存器,并对应data这个变量
# "d"(port)表示输入为port对应的数据并放到edx相关的寄存器
# 也就是说,%1作为输入,对应edx相关的寄存器
# %0作为输出,对应eax相关的寄存器
# 整体上,这段内联汇编代码表示从port中读取一个字节的数据并返回
38 static inline uint8_t
39 inb(uint16_t port) {
40 uint8_t data;
41 asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
42 return data;
43 }
# 汇编代码中没有输出,全都是输入
# outb就是往指定地方写入指定数据
55 static inline void
56 outb(uint16_t port, uint8_t data) {
57 asm volatile ("outb %0, %1" :: "a" (data), "d" (port));
58 }
# repne 不等的时候重复。rep指令又叫做重复串操作指令,它是一个前缀,位于一条指令之前,
# 这条指令将会一直被重复执行,并且直到计数寄存器的值满足某个条件。repnz指令是当计数器%ecx的
# 值不为零是就一直重复后面的串操作指令。那么被重复调用的指令就是insl指令。
# ins指令可从第一个源操作数所指的外设端口输入n个字节到由第二个源操作数指定的存储器中。
# insl表示一次读4个字节。
# 该函数就是利用汇编代码从硬盘将指定扇区(在outb中已经通知IO是哪个扇区了,IO自己跑到对应的
# 扇区做好读取准备,到这个函数的时候,硬盘IO已经到了指定扇区,就等着读取了)读取指定量的数据
# 到指定的内存区域。输入参数都是给insl指令的,这是个操作IO的指令。
# 切记切记,读取哪个扇区是在该函数之前就已经通知了硬盘IO的,这个函数只是负责从这个扇区中读取
# 多少字节到指定内存!!!
45 static inline void
46 insl(uint32_t port, void *addr, int cnt) {
47 asm volatile (
48 "cld;"
49 "repne; insl;"
50 : "=D" (addr), "=c" (cnt)
51 : "d" (port), "0" (addr), "1" (cnt)
52 : "memory", "cc");
53 }
2.按照3个program header(他仨在上一步已经载入内存了)中记录的信息,将3个segment分别从硬盘载入内存,这三个segment都还在硬盘里呢。
90 /* bootmain - the entry of bootloader */
91 void
92 bootmain(void) {
......
111 /* */
# 分别将三个program header描述的segment从硬盘载入内存
112 for (; ph < eph; ph ++) {
# 8个扇区4KB,而0x1000=4KB,所以这里恰好需要继续从硬盘读取,已经读到
# 内存的数据根本没有segment!!!
# 这里就是根据program header中描述的信息,将位于elf文件指定位置(ph->p_offset)
# 指定大小(ph->p_memsz)的内容加载到内存指定位置(ph->p_va & 0xFFFFFF)
# 3个segment全部载入内存后(实际只有两个有效的加载了进来,GNU_STACK这
# 个segment不会载入内存),也就意味着现在内核已经全部载入内存了!!!
113 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
114 }
......
129 }
3.将CPU控制权交给kernel
91 void
92 bootmain(void) {
93 // read the 1st page off disk
94 /* 一口气读入8个扇区,其中还包括存放bootloader 代码的扇区 */
95 readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
96
97 // is this a valid ELF?
98 if (ELFHDR->e_magic != ELF_MAGIC) {
99 goto bad;
100 }
101
102 struct proghdr *ph, *eph;
103
104 // load each program segment (ignores ph flags)
105 /* ELF头部有描述ELF文件应加载到内存什么位置的描述表
106 * 这一步是从elf 文件中获取程序描述表头
107 */
108 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
109 eph = ph + ELFHDR->e_phnum;
110
111 /* 按照描述表将ELF文件中数据载入内存 */
112 for (; ph < eph; ph ++) {
113 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
114 }
115
116 // call the entry point from the ELF header
117 // note: does not return
121 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
122
123 bad:
124 outw(0x8A00, 0x8A00);
125 outw(0x8A00, 0x8E00);
126
127 /* do nothing */
128 while (1);
129 }
通过使用objdump查看kernel,也可以发现其入口地址为0x100000,即kern_init()
通过使用GDB,在对应的源文件处打断点
file obj/bootblock.o
b boot/bootmain.c:121
连续几个si
会发现pc会指向0x100000这个地址,通过tool/kernel.ld链接脚本可知kern_init()函数
被放到了这个地址,也就是说,以下代码
121 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
实际上调用的是可执行文件kernel里面的kern_init()函数!!!
(gdb) file obj/bootblock.o
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
A program is being debugged already.
Reading symbols from obj/bootblock.o...done.
(gdb) break boot/bootmain.c:121
Breakpoint 1 at 0x7d63: file boot/bootmain.c, line 121.
(gdb) continue
Continuing.
Breakpoint 1, bootmain () at boot/bootmain.c:121
(gdb) x /10i $pc
=> 0x7d63 <bootmain+84>: mov 0x7dec,%ax
0x7d66 <bootmain+87>: add %al,(%bx,%si)
0x7d68 <bootmain+89>: mov 0x18(%bx,%si),%ax
0x7d6b <bootmain+92>: and $0xffff,%ax
0x7d6e <bootmain+95>: incw (%bx,%si)
0x7d70 <bootmain+97>: call *%ax------------就是这里
0x7d72 <bootmain+99>: mov $0x8a00,%dx
0x7d75 <bootmain+102>: (bad)
0x7d76 <bootmain+103>: decw 0x66d0(%bx,%di)
0x7d7a <bootmain+107>: out %ax,(%dx)
(gdb) b *0x7d70
Breakpoint 2 at 0x7d70: file boot/bootmain.c, line 121.
(gdb) continue
Continuing.
Breakpoint 2, 0x00007d70 in bootmain () at boot/bootmain.c:121
(gdb) x /1i $pc
=> 0x7d70 <bootmain+97>: call *%ax
(gdb) si
0x00100000 in ?? ()-------------------------到了kern_init()
(gdb) x /10i $pc
=> 0x100000: push %bp
0x100001: mov %sp,%bp
0x100003: sub $0x18,%sp
0x100006: mov $0xfd80,%dx
0x100009: adc %al,(%bx,%si)
0x10000b: mov $0xea16,%ax
0x10000e: adc %al,(%bx,%si)
0x100010: sub %ax,%dx
0x100012: mov %dx,%ax
0x100014: sub $0x4,%sp
练习五
函数堆栈
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 |
| 上一层[ebp] | <-------- [ebp]
| 局部变量 | 低位地址
如上图所示,ebp最最关键的特性就是通过当时的ebp值“向上(栈底方向)”获取返回地址、参数值,“向下(栈顶方向)”获取函数局部变量值,这也就意味着每次函数调用的时候,只要找到ebp,就可以得到返回地址、参数、局部变量等值!!!同时,又因为此时(esp的值已经给了ebp)ebp本身指向的内存地址处保存的是ebp上一次的值(同样是个内存地址),这个值属于调用当前函数的函数(父函数),所以要想知道当前函数的父函数是谁,只要查看当前ebp指向的那块内存地址下面保存的值就行,这个值就是ebp上一次的值,找到它,跳到它的实际位置,“向下”即可得到父函数的局部变量,向上就可以得到父函数的返回值、参数值等。以此类推,就得到了函数调用栈。
使用GDB验证
(gdb) info break
Num Type Disp Enb Address What
6 breakpoint keep y 0x00100000 in kern_init at kern/init/init.c:25
7 breakpoint keep y 0x00102a7d in pmm_init at kern/mm/pmm.c:96
8 breakpoint keep y 0x0010297b in gdt_init at kern/mm/pmm.c:76
(gdb) continue
Continuing.
Breakpoint 6, kern_init () at kern/init/init.c:25
(gdb) info reg
eax 0x100000 1048576
ecx 0x0 0
edx 0x0 0
ebx 0x10094 65684
esp 0x7bec 0x7bec
ebp 0x7bf8 0x7bf8
esi 0x10094 65684
edi 0x0 0
eip 0x100000 0x100000 <kern_init>
eflags 0x6 [ PF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x10 16
gs 0x10 16
设置三个断点,第一次断在kern_init()处,此时ebp=0x7bf8
,0x7bf8地址保存的内容是ebp上一次的值,对本次验证没用,只要记住当前的ebp就好。
(gdb) continue
Continuing.
Breakpoint 7, pmm_init () at kern/mm/pmm.c:96
(gdb) info reg
eax 0x0 0
ecx 0x0 0
edx 0x103722 1062690
ebx 0x10094 65684
esp 0x7bcc 0x7bcc
ebp 0x7be8 0x7be8
esi 0x10094 65684
edi 0x0 0
eip 0x102a7d 0x102a7d <pmm_init>
eflags 0x6 [ PF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x10 16
gs 0x10 16
(gdb) x /10w 0x7be8
0x7be8: 0x00007bf8 0x00007d72 0x00000000 0x00000000
0x7bf8: 0x00000000 0x00007c4f 0xc031fcfa 0xc08ed88e
0x7c08: 0x64e4d08e 0xfa7502a8
这次断在pmm_init()处,此时ebp=0x7be8
,查看0x7be8地址处的值为0x00007bf8,正好就是ebp上一个值。返回地址并不是是0x00007d72,因为在这个地址打断点根本不会断住。
(gdb) continue
Continuing.
Breakpoint 8, gdt_init () at kern/mm/pmm.c:76
(gdb) info reg
eax 0x0 0
ecx 0x0 0
edx 0x103722 1062690
ebx 0x10094 65684
esp 0x7bc4 0x7bc4
ebp 0x7bc8 0x7bc8
esi 0x10094 65684
edi 0x0 0
eip 0x10297b 0x10297b <gdt_init>
eflags 0x6 [ PF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x10 16
gs 0x10 16
(gdb) x /10w 0x7bc8
0x7bc8: 0x00007be8 0x00100055 0x00000000 0x00000000
0x7bd8: 0x00000000 0x00103560 0x00010094 0x00000000
0x7be8: 0x00007bf8 0x00007d72
(gdb) b *0x00100055
Breakpoint 9 at 0x100055: file kern/init/init.c, line 44.
(gdb) continue
Continuing.
Breakpoint 9, kern_init () at kern/init/init.c:44
(gdb) info reg
eax 0x28 40
ecx 0x0 0
edx 0x103722 1062690
ebx 0x10094 65684
esp 0x7bd0 0x7bd0
ebp 0x7be8 0x7be8
esi 0x10094 65684
edi 0x0 0
eip 0x100055 0x100055 <kern_init+85>
eflags 0x12 [ AF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x23 35
gs 0x23 35
这次断在gdt_init()处,此时ebp=0x7bc8
,查看0x7bc8地址处的值为0x00007be8,正好就是ebp上一个值。返回地址应该是0x00100055,所以设置断点,并继续运行,断点位置直接就是pic_init(),没有经过pmm_init()。
95 void
96 pmm_init(void) {
97 gdt_init();
98 }
一点分析
static __noinline uint32_t
read_eip(void) {
uint32_t eip;
asm volatile("movl 4(%%ebp), %0" : "=r" (eip));
return eip;
}
寄存器eip并不能通过mov这类操作来修改,因此要想查看eip的值,除了在GDB比较容易以外,想在代码中查看其实挺困难的,所以这里用了一个非常巧妙的方法来间接查看eip的值。由于read_eip函数肯定不能被内联(__noinline),所以这就是一个普通函数,也就是说,进入该函数的时候,函数堆栈也要发生变化,ebp也是要变的!!!函数read_eip里面的内联汇编使用的ebp,此时就是read_eip自己的ebp,4(%%ebp)
就是read_eip返回时要跳转的地址,而根据代码的上下文,我们想要的eip不是read_eip的eip,而是调用read_eip位置处的那个eip(调用read_eip的目的就是知道当前位置的eip,只是一旦调了read_eip,那么eip也就是read_eip的了),这个eip恰恰就是read_eip返回时要跳转的那个地址,也就是read_eip中的4(%%ebp)
!!!所以通过read_eip()获取当前的eip,是没问题的!!!
练习六
设置中断的整体流程
int
kern_init(void) {
......
# 这里面先初始化物理内存,主要是会把GDT在这里重新初始化一遍
pmm_init(); // init physical memory management
# 从这里开始设置中断
# 注意这里的顺序,如果要设置外设的中断(例如时钟中断),一定要在使能中断之前进行
pic_init(); // init interrupt controller(8259A)
idt_init(); // init interrupt descriptor table
# 设置时钟芯片的时钟中断功能,以便产生时钟中断
clock_init(); // init clock interrupt
# 在bootloader里停用中断,这里要使能中断
intr_enable(); // enable irq interrupt
......
while (1);
}
初始化中断向量表(idt_init)
vectors.S
1 # handler
2 .text
3 .globl __alltraps
4 .globl vector0
5 vector0:
6 pushl $0
7 pushl $0
8 jmp __alltraps
9 .globl vector1
10 vector1:
11 pushl $0
12 pushl $1
13 jmp __alltraps
......
1272 .globl vector255
1273 vector255:
1274 pushl $0
1275 pushl $255
1276 jmp __alltraps
(注意,vectors.S是由tools/vector.c编译出来的工具生成的,相当于是一个手写的汇编代码文件,但在ucore中,该汇编文件是源码中自带的,并没有编译tools/vector.c,相当于现成的了)
从vector0到vector255都是中断服务例程(的入口地址),vectorNUM是中断服务例程的入口地址。从代码中可以看出,所有的中断最终都跑进了__alltraps
中,即__alltraps
函数内部才去区分具体的中断号以及服务。这样做的好处是在一定程度上减少冗余代码,在__alltraps
里已经写好了所有中断服务例程在真正开始执行之前都需要做的相同的操作(例如压栈、初始化结构体等),然后再跳进一个C函数trap(struct trapframe *tf)
,由这个C函数去具体区分中断号并执行对应的中断服务例程。这样做的好处非常明显,在一开始就用简单、统一的汇编代码安排好全部的中断号和中断向量(此时并不是所有中断号都有对应的中断服务例程),后面想要增加某个自己的中断服务时,直接在C函数trap(struct trapframe *tf)
里面的switch...case...
语句中增加相应的中断号并添加对应的中断服务例程就可以了,全部都是C代码,容易编码,完全不用管vector相关的汇编代码了!!!否则就得在vectors.S这个汇编文件中增加自己的中断向量(比如vector128)并为其添加对应的中断处理函数比如tmp_test
,整体类似如下代码,
.globl vector128
vector128:
pushl $0
pushl $255
jmp tmp_test
而且tmp_test内部还得先进行一大堆其他类似函数也做了的、完全相同的诸如压栈等操作之后才能开始干自己的事。所有中断都跑进__alltraps
还有另外一个好处,就是在GDT中设置对应的段描述符时会很容易,仅仅只需设置“一个”能够指向__alltraps
的段描述符来最终指向__alltraps
所在的段,那么所有中断在触发后,都只需找到这个段就能找到自己的服务例程,直接涉及中断的段其实就__alltraps
一个,从内存管理上看,会非常简洁清晰!!!
1278 # vector table
1279 .data
1280 .globl __vectors
1281 __vectors:
1282 .long vector0
1283 .long vector1
......
1533 .long vector251
1534 .long vector252
1535 .long vector253
1536 .long vector254
1537 .long vector255
从这里可以看出,__vectors
是个全局数组(.globl),从vector0到vector255所有中断服务例程的入口地址都按顺序写到了这个数组,数组下标和vectorNUM的NUM对应,其实就是中断号,所以可通过中断号作为__vectors
数组的索引来查找对应的中断服务例程(的入口地址)。
编写中断处理函数
62 struct trapframe {
63 struct pushregs tf_regs;
64 uint16_t tf_gs;
65 uint16_t tf_padding0;
66 uint16_t tf_fs;
67 uint16_t tf_padding1;
68 uint16_t tf_es;
69 uint16_t tf_padding2;
70 uint16_t tf_ds;
71 uint16_t tf_padding3;
72 uint32_t tf_trapno;
73 /* below here defined by x86 hardware */
74 uint32_t tf_err;
75 uintptr_t tf_eip;
76 uint16_t tf_cs;
77 uint16_t tf_padding4;
78 uint32_t tf_eflags;
79 /* below here only when crossing rings, such as from user to kernel */
80 uintptr_t tf_esp;
81 uint16_t tf_ss;
82 uint16_t tf_padding5;
83 } __attribute__((packed));
3 # vectors.S sends all traps here.
4 .text
5 .globl __alltraps
6 __alltraps:
7 # push registers to build a trap frame
8 # therefore make the stack look like a struct trapframe
9 pushl %ds
10 pushl %es
11 pushl %fs
12 pushl %gs
13 pushal
14
15 # load GD_KDATA into %ds and %es to set up data segments for kernel
16 movl $GD_KDATA, %eax
17 movw %ax, %ds
18 movw %ax, %es
19
20 # push %esp to pass a pointer to the trapframe as an argument to trap()
21 pushl %esp
22
23 # call trap(tf), where tf=%esp
24 call trap
25
26 # pop the pushed stack pointer
27 popl %esp
29 # return falls through to trapret...
30 .globl __trapret
31 __trapret:
32 # restore registers from stack
33 popal
34
35 # restore %ds, %es, %fs and %gs
36 popl %gs
37 popl %fs
38 popl %es
39 popl %ds
40
41 # get rid of the trap number and error code
42 addl $0x8, %esp
43 iret
触发中断时,所有的中断都会调用__alltraps
函数,从上面的代码可以看出来,在调用trap
之前,会将相关的寄存器压栈,把相关的内容压栈的过程也就是填充struct trapframe
的过程以便将该结构体作为trap
的参数。这个结构体保存在栈中,所以传结构体地址作为参数时,直接把esp压栈就行,被压栈的这个esp的值就是结构体的地址!
这里要注意一个细节,在结构体定义中,tf_gs
排在最前(只看gs/fs/es/ds这几个寄存器),也就是地址最低的位置,而压栈的时候,gs
最后压栈,考虑到内核栈栈底在高地址,栈顶在低地址,所以gs
是被压到了低地址,这样就保证了压栈顺序和结构体数据定义是吻合的!!!另外,使用pushl
压栈,应该是一口气压入4B,但是结构体中tf_gs
只有2B,所以才出现了tf_padding0
这种数据成员用于占位。还有就是,在vectors.S里面调用__alltraps
之前,也有压栈动作,将$0
和中断号压栈,然后直接用一个跳转指令jmp
调用__alltraps
,所以栈可以直接映射tf
结构体,里面的中断号就是这么来的。结构体定义中,/* below here defined by x86 hardware */
这句话往下的内容,不是人为压栈设置的,而是产生中断的时候,由硬件自己自动完成压栈做到的,所以尽管代码里没写,但不代表没做这个行为,也不代表内存里没有相应的数据!所以,放心大胆的使用传递给trap
的参数吧~
从trap
返回后,顺着就进入__trapret
,该函数负责将先前寄存器的值弹栈,并调用iret
指令返回到调用__alltraps
函数的地方,结束中断过程。注意,iret
指令本身就是从中断返回的指令。