因为只有引导代码中使用了as86的编译器,linux0.12其他汇编都是基于GNU as汇编的,这是背景。
head.s是在gnu as下编译的,使用的是AT&T汇编模式。主要完成一些GDT的初始化,然后跳转到main.c函数执行,这两者如何顺利调用的,今天研究一下。
下面是linus给出的关键源码:
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
/*
* Setup_paging
*
* This routine sets up paging by setting the page bit
* in cr0. The page tables are set up, identity-mapping
* the first 16MB. The pager assumes that no illegal
* addresses are produced (ie >4Mb on a 4Mb machine).
*
* NOTE! Although all physical memory should be identity
* mapped by this routine, only the kernel page functions
* use the >1Mb addresses directly. All "normal" functions
* use just the lower 1Mb, or the local data space, which
* will be mapped to some other place - mm keeps track of
* that.
*
* For those with more memory than 16 Mb - tough luck. I've
* not got it, why should you :-) The source is here. Change
* it. (Seriously - it shouldn't be too difficult. Mostly
* change some constants etc. I left it at 16Mb, as my machine
* even cannot be extended past that (ok, but it was cheap :-)
* I've tried to show which constants to change by having
* some kind of marker at them (search for "16Mb"), but I
* won't guarantee that's all :-( )
*/
.align 2
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,pg_dir /* set present bit/user r/w */
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
setup_paging的实现这次不研究,重点在于main函数。
首先看看c代码的调用模式
依旧看一个小c代码的汇编
void swap(int*a ,int *b)
{
int c;
c=*a;
*a=*b;
*b=c;
}
int main()
{
int a,b;
a=1,b=2;
swap(&a,&b);
return a+b;
}
在mingw32下gcc -Wall -S -o exch.s exch.c
汇编代码为
.file "exch.c"
.text
.globl _swap
.def _swap; .scl 2; .type 32; .endef
_swap:
LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
movl (%eax), %edx
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 12(%ebp), %eax
movl -4(%ebp), %edx
movl %edx, (%eax)
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE0:
.def ___main; .scl 2; .type 32; .endef
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 28(%esp)
movl $2, 24(%esp)
leal 24(%esp), %eax
movl %eax, 4(%esp)
leal 28(%esp), %eax
movl %eax, (%esp)
call _swap
movl 28(%esp), %edx
movl 24(%esp), %eax
addl %edx, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1:
去除链接信息,最简洁的表示为
_swap:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
movl (%eax), %edx
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 12(%ebp), %eax
movl -4(%ebp), %edx
movl %edx, (%eax)
leave
ret
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 28(%esp)
movl $2, 24(%esp)
leal 24(%esp), %eax
movl %eax, 4(%esp)
leal 28(%esp), %eax
movl %eax, (%esp)
call _swap
movl 28(%esp), %edx
movl 24(%esp), %eax
addl %edx, %eax
leave
ret
看看栈帧是如何利用的。
首先理解几个汇编指令
call
把返回地址压入栈中,并跳转到被调函数开始执行。返回地址是紧随call指令后的指令地址。
ret
弹出栈顶的地址,并跳转到该地址执行
leave
恢复ebp和esp,因为在每个被调函数的第一个栈空间是保存的调用者函数的ebp地址,自己的ebp就指向这个地址,所以leave就是将esp跳转回来,并恢复原来ebp
等价于
movl %ebp,%esp
popl %ebp
看到上面示意图是有这样的调用顺序的
main将esp自减32,32的栈空间用于存放数据
将a、b的数据压栈到+28的位置和+24的位置
将参数a、b的地址压栈到如图位置
call指令压入返回地址跳转到swap的栈空间
swap依旧是先将esp-16,开辟一段栈空间
然后保存main的ebp,swap的ebp也就从这个地址开始
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
是将&b的内容存到eax中也就是b的地址
然后取地址中的数存放eax,就是b的值,再将b的值压栈。
同理a也一样
执行leave,将esp指向swap栈空间开始的地方,然后恢复main的ebp
ret 此时栈顶回到了main中,就是返回地址的地方,pop出这个地址去执行
明白此,在看看linus是如何完成这个跳转的
setup_paging的最后一个指令就是ret
而setup_paging不是使用的call而是jmp所以就没有自动完成返回地址的压栈,
在jmp前一个指令,有
pushl $main
jmp setup_paging
所以会pop出main的地址然后去执行而mian前面的就是给main传递的参数。
而jmp main前面一个指令是main返回时的跳转地址,这里是一个无限循环,也就是系统会一直run。