Linux PC 开机全过程

2.1 第一行Linux代码

源码:https://elixir.bootlin.com/linux/latest/source/arch/x86/boot/header.S

BootLoader将Linux Kernel加载到内存地址0x10000后,跳过512字节(0x200字节,1个扇区的长度),jump到0x10200处执行。

在0x10200之前,我们先看看0x10000开始的512字节里有什么

旁路陷阱——不再支持的软盘启动

曾经,Linux支持从存储介质的零扇区启动。如果Linux Kernel真的被放在存储介质的首扇区,那么开机后,BIOS将软盘的内容复制到内存的0x7c00处,并从此处开始执行。既然现在已经不支持这种启动方式,Linux将这512字节用于在显示器上打印一段错误信息:

Use a boot loader.

Remove disk and press any key to reboot...

我们具体看看它是怎么做的:

	.global bootsect_start
bootsect_start:
#ifdef CONFIG_EFI_STUB
	# "MZ", MS-DOS header
	.byte 0x4d
	.byte 0x5a
#endif

	# Normalize the start address
	ljmp	$BOOTSEG, $start2

bootsect_start是这个文件的开头,那么第一条指令就是ljmp $BOOTSEG, $start2。一个long jump不仅跳转到了07c0:$start2的位置,还将前面提到的特殊的寻址方式还原为标准的实地址模式。

start2:
	movw	%cs, %ax
	movw	%ax, %ds
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp
	sti
	cld

	movw	$bugger_off_msg, %si

msg_loop:
	lodsb
	andb	%al, %al
	jz	bs_die
	movb	$0xe, %ah
	movw	$7, %bx
	int	$0x10
	jmp	msg_loop

start2:
使用cs代码段寄存器,将ax、ds、es、ss统统初始化了一遍。
然后通过经典的xor异或两个相同的寄存器,将sp栈指针寄存器置零。
sti指令(Set Interrupt Flag),将中断打开。此时可以放心地打开中断,因为刚从BIOS那里过来,BIOS的中断都还可以用呢。

将bugger_off_msg的地址放到si寄存中(Source Index Register)。
在显示器上打印$bugger_off_msg内容:
BIOS的众多中断中,int 10h用于显示器相关操作。其具体行为,取决于AH寄存器中存储的function code。这里选择AH=0eh,即打印单个字符并将光标向右移动1位。中断处理函数会将AL中的内容作为字符,打印到显示器上。
为了打印整个字符串,这里使用jmp msg_loop实现循环,并在and %al, al结果AL为0时,jz bs_die跳出循环。
Intel字符串打印指令集基本都支持正序和逆序,cld指令(Clear Direction Flag),清除方向标志位,则后面lodsb执行后,si和di寄存器都会自增1。lodsb指令等价于

movsb %si, %al
inc %si

如此完成取字符的操作,并放到AL中。
一次取1个字节,即2个字符,因此第一个循环您会看到Us,之后看到e等。
如果AL不为\0,则

	movb	$0xe, %ah
	movw	$7, %bx
	int	$0x10

调用BIOS中断,显示字符。

bs_die:
	# Allow the user to press a key, then reboot
	xorw	%ax, %ax
	int	$0x16
	int	$0x19

	# int 0x19 should never return.  In case it does anyway,
	# invoke the BIOS reset code...
	ljmp	$0xf000,$0xfff0

bs_die的作用是允许用户按任意键重启。
既然是按键,那我们当然要学习下键盘中断的知识。
当int 16H中断被触发,CPU会检查AH的内容,进而决定采取的具体行为。
AH为0时,CPU会阻塞式地读取按键值,并将按键值放到AL去。
既然想让AH为0,那么xorw %ax, %ax的作用就显而易见了。
如果用户一直不按键,那么CPU就会陷在这个中断里一直不出来。
当用户终于按了任意键,kernel压根就没去看AL的值,直接去了int 19H。
int 19H的作用在此处是重启,但它的实际行为和重启并不相同。它会将MBR加载到07c00h处,重新加载BootLoader的过程。

真正的入口

	.globl	_start
_start:
		# Explicitly enter this as bytes, or the assembler
		# tries to generate a 3-byte jump here, which causes
		# everything else to push off to the wrong offset.
		.byte	0xeb		# short (2-byte) jump
		.byte	start_of_setup-1f

start:
这个文件真正的入口位于512字节之后。
此刻,我们需要从前面的故事中走出来,尤其是将我们的位置从0x07c00切换到0x10200。
这里想要实现一个2个字节的short jump(TODO: 补充原因),但是编译器默认jump为3个字节的jump,因此手动写opcode为0xeb然后计算标号start_of_setup和标号1之间的距离。short jump的操作数并不是相对段基址的偏移,而是相对此刻IP寄存器的偏移。也即short jump并不是“我想去那里”,而是“我想向前/向后走n步”。

	.section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
	movw	%ds, %ax
	movw	%ax, %es
	cld

# Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,
# which happened to work by accident for the old code.  Recalculate the stack
# pointer if %ss is invalid.  Otherwise leave it alone, LOADLIN sets up the
# stack behind its own code, so we can't blindly put it directly past the heap.

	movw	%ss, %dx
	cmpw	%ax, %dx	# %ds == %ss?
	movw	%sp, %dx
	je	2f		# -> assume %sp is reasonably set

	# Invalid %ss, make up a new stack
	movw	$_end, %dx
	testb	$CAN_USE_HEAP, loadflags
	jz	1f
	movw	heap_end_ptr, %dx
1:	addw	$STACK_SIZE, %dx
	jnc	2f
	xorw	%dx, %dx	# Prevent wraparound

2:	# Now %dx should point to the end of our stack space
	andw	$~3, %dx	# dword align (might as well...)
	jnz	3f
	movw	$0xfffc, %dx	# Make sure we're not zero
3:	movw	%ax, %ss
	movzwl	%dx, %esp	# Clear upper half of %esp
	sti			# Now we should have a working stack

# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
	pushw	%ds
	pushw	$6f
	lretw

start_of_setup:
借助ax的帮助,强制将es(extra stack, 额外的段寄存器)赋值为ds(data stack,数据段寄存器)的值。TODO:说明作用

6:

# Check signature at end of setup
	cmpl	$0x5a5aaa55, setup_sig
	jne	setup_bad

https://lkml.org/lkml/2018/3/21/226
cmpl $0x5a5aaa55, setup_sig的用意是检测setup_sig是否等于0x5a5aa55这个魔法值,即检测setup.bin是否加载在了正确的位置。否则,跳转到setup_bad去报错。

# Setup corrupt somehow...
setup_bad:
	movl	$setup_corrupt, %eax
	calll	puts
	# Fall through...

	.globl	die
	.type	die, @function
die:
	hlt
	jmp	die

	.size	die, .-die

	.section ".initdata", "a"
setup_corrupt:
	.byte	7
	.string	"No setup signature found...\n"

打印No setup signature found...

# Zero the bss
	movw	$__bss_start, %di
	movw	$_end+3, %cx
	xorl	%eax, %eax
	subw	%di, %cx
	shrw	$2, %cx
	rep; stosl

BSS段置零
TODO

# Jump to C code (should not return)
	calll	main

从此,告别汇编,跳转到C语言的世界,并且理论上不会返回。