
所谓 “穷” 字,就是在洞穴里卖苦力!当然了,现在不会有人在洞穴里卖苦力了!但是,小小的一间办公室又锁住了多少人的肉与灵呢?
末尾分享一些不错的资源!!



今天想聊一下 main 函数是怎么开始执行的,感觉是一个很无聊的话题。其实的确比较无聊,这不过只是想在上篇文章的介绍中,更直观的感受一下汇编指令 call 和 ret 这两个指令,还有体会一下“栈”中保存函数返回地址这个事情。


上篇文章:你这个程序员懂函数调用么
再上一篇文章:从内存地址 0x7c00 思考程序员的差距
0x01:C 语言的 main 函数
在学习 C 语言的时候,书上告诉我们说 main 函数是程序的主函数,是入口函数!main 函数是我们程序员写代码时的一个入口,也就是说,我们写的 C 语言的程序是从 main 函数开始执行的。注意,是我们写的代码的入口,并不是真正的入口。
那么 main 函数是怎么开始的呢?是由谁调用的呢?我们简单来看一下。
随便写段 C 语言的代码,然后在第一行代码处设置断点,然后调试运行起来,查看调用栈。

上图是断在 main 函数处时的调用栈,调用栈可以反映函数的调用关系。在上面的图中可以看到,在进入 main 函数之前,还是有层层调用的,下面 3 行,可以看到是执行了 ntdll 和 kernel32 这两个 dll 中的代码,在倒数第四行中可以看到,开始调用 mainCRTStartup 函数,然后一层层的到了我们的 main 函数处。
0x02:Linux 操作系统的 main 函数是谁调用的呢?
Linux 是汇编和 C 语言实现的,那么在 Linux 源码中的 C 语言部分是否有 main 函数呢?那是肯定的!那 Linux 源码中的 main 函数是由谁调用的呢?
这里参考的是 Linux 0.11 的源码,在 head.s 源码中,给出了对 main 函数的调用的代码,代码如下:
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.上面的代码中,连续的 pushl 后面有一个 jmp 指令,我们看一下 setup_paging 部分的代码。
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 呢?其实完全不用看!在 setup_paging 中没有栈相关的操作。最后有一个 ret 指令!在上篇文章中应该提过,ret 时会把栈顶位置的值给到指令指针寄存器中。
当前的栈顶的值就是 _main,也就 setup_paging 返回后,直接是执行 _main 函数了。再回看一下上面的代码。
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.上面的代码中,在不考虑 jmp 的情况下,连续 pushl 可以看做如下的代码:
pushl $0
pushl $0
pushl $0
pushl $L6
call $_main前 3 个 pushl 是 main 函数的参数,依次是 env、argv 和 argc,最后的 $L6 是 main 函数的返回地址。看注释 main 函数是不会返回的。所以这个返回地址的标号也比较有意思,哈哈!L6,老 6?不返回就命名为“老六”?!!
0x03:类似的例子
在 Linux 中还有类似的代码吗,是有的,随便拿一处的代码来看一下吧。
代码如下:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
...
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}上面的代码还是出自 Linux 0.11 的源码中,在 main.c 这个源码中。
move_to_user_mode() 是从内核态切换到用户态,使用的方式中断。看上面代码的第 8 行,指令是 iret 指令,这是中断返回的指令。在上面的代码中是没有进行中断调用的,那它怎么进行中断返回的呢?也是借助栈顶的值返回。它自己压栈构造了一个中断返回地址,然后调用 iret 来完成中断返回,从而完成了内核态到用户态的切换。
0x04:最后...
终归 main 函数只是程序员写代码的入口,而不是真正的程序的入口,无论是对于 PE 文件、ELF 文件,还是 MachO 的文件,里面都有实际的程序入口的地址。我们的可执行文件在生成时,链接器会提供很多操作系统相关的数据到可执行文件中,便于操作系统的加载。当然了,不同的操作系统的文件格式也是不同,因此这些可执行的二进制文件想要直接跨操作系统执行……不行的!
分享些觉得不错的资源吧!两本电子书吧,里面的内容感觉挺不错的!
阶层跃升之道是998元藏经阁精选书籍.pdf https://pan.quark.cn/s/e06b8a7ad805
绝密人性天书.pdf https://pan.quark.cn/s/d75ab25b798b