谁执行了我的main函数

来源:互联网 发布:cs1.6参数优化 编辑:程序博客网 时间:2024/05/16 15:16

接着说“调用”,前面我们提到了一点系统调用,又略微详细的说了一下函数调用,现在接着说一下程序调用,应该说是执行一个可执行程序。

前面的几篇文章都在围绕着函数调用栈来说,从main函数开始到每个函数的调用和返回,那么在main函数之前和main函数之后,也就是开始执行程序的main函数之前以及main函数返回后又有什么样的动作呢?


当我们在终端输入一个可执行文件的路径名时,如:

linux> ./myexec


shell就会创建一个子进程,然后让子进程负责执行myexec这个程序。当然也可以通过命令行参数让shell不创建一个子进程来执行程序,而是直接用shell本身进程来执行这个程序,这样执行的后果就是要执行的程序会完全替换当前的shell程序(一般也就不会再返回给shell了)。


要想把程序执行的过程都说清楚,这个概念太大了,需要很多知识,不是一篇文章就能说完的。所以我们通过几个简单的问题来先从表面了解一下执行程序的概念:

1、为什么写程序的时候(C语言)都要从main开始? 为什么一定要是main函数?

2、前文我们讲函数调用时所使用的栈在哪里?


第一个问题,why main?

从学习C语言开始到现在是否有人想过,why main? 为什么一定要从main函数开始呢?换一个函数行不行?


为了回答这个问题,我们得先回到编译的时候。说到编译你会想到什么? 使用集成开发环境时的一个编译按钮? 一个简单的gcc命令?F5或者什么的快捷编译键?

可能在每个人接触程序至今都有对编译的不同印象,那么编译到底包含什么? 有点基础的人可能会回答:包括预编译(把头文件包含进来,把宏替换了),编译(得到汇编语言程序),汇编(得到二进制目标文件.o),链接(得到可执行文件)。


我们抛开前三个不说(编译可是个力气活,个人觉得不比写kernel简单),单说最后一步链接。现在有一个C程序,

int bar(int a){    return a;}void foo(void){    bar(3);}int main(int argc, char *argv[]){    foo();    return 0;}

 

编译成可执行程序gcc myprog.c -o myprog,然后我们看一下myprog里的内容:

myprog:     文件格式 elf64-x86-64Disassembly of section .init:00000000004003a8 <_init>:  4003a8:       48 83 ec 08             sub    $0x8,%rsp  4003ac:       48 8b 05 45 0c 20 00    mov    0x200c45(%rip),%rax        # 600ff8 <_DYNAMIC+0x1d0>  4003b3:       48 85 c0                test   %rax,%rax  4003b6:       74 05                   je     4003bd <_init+0x15>  4003b8:       e8 33 00 00 00          callq  4003f0 <__gmon_start__@plt>  4003bd:       48 83 c4 08             add    $0x8,%rsp  4003c1:       c3                      retq   Disassembly of section .plt:00000000004003d0 <__libc_start_main@plt-0x10>:  4003d0:       ff 35 32 0c 20 00       pushq  0x200c32(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>  4003d6:       ff 25 34 0c 20 00       jmpq   *0x200c34(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>  4003dc:       0f 1f 40 00             nopl   0x0(%rax)00000000004003e0 <__libc_start_main@plt>:  4003e0:       ff 25 32 0c 20 00       jmpq   *0x200c32(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>  4003e6:       68 00 00 00 00          pushq  $0x0  4003eb:       e9 e0 ff ff ff          jmpq   4003d0 <_init+0x28>Disassembly of section .text:0000000000400400 <_start>:  400400:       31 ed                   xor    %ebp,%ebp  400402:       49 89 d1                mov    %rdx,%r9  400405:       5e                      pop    %rsi  400406:       48 89 e2                mov    %rsp,%rdx  400409:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp  40040d:       50                      push   %rax  40040e:       54                      push   %rsp  40040f:       49 c7 c0 a0 05 40 00    mov    $0x4005a0,%r8  400416:       48 c7 c1 30 05 40 00    mov    $0x400530,%rcx  40041d:       48 c7 c7 0c 05 40 00    mov    $0x40050c,%rdi  400424:       e8 b7 ff ff ff          callq  4003e0 <__libc_start_main@plt>  400429:       f4                      hlt      40042a:       66 90                   xchg   %ax,%ax  40042c:       0f 1f 40 00             nopl   0x0(%rax)00000000004004f0 <bar>:  4004f0:       55                      push   %rbp  4004f1:       48 89 e5                mov    %rsp,%rbp............00000000004004fc <foo>:  4004fc:       55                      push   %rbp  4004fd:       48 89 e5                mov    %rsp,%rbp............000000000040050c <main>:  40050c:       55                      push   %rbp  40050d:       48 89 e5                mov    %rsp,%rbp  400510:       48 83 ec 10             sub    $0x10,%rsp  400514:       89 7d fc                mov    %edi,-0x4(%rbp)  400517:       48 89 75 f0             mov    %rsi,-0x10(%rbp)  40051b:       e8 dc ff ff ff          callq  4004fc <foo>  400520:       b8 00 00 00 00          mov    $0x0,%eax  400525:       c9                      leaveq   400526:       c3                      retq     400527:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)  40052e:       00 00 0000000000400530 <__libc_csu_init>:  400530:       41 57                   push   %r15  400532:       41 89 ff                mov    %edi,%r15d  ......  ......00000000004005a0 <__libc_csu_fini>:  4005a0:       f3 c3                   repz retq   4005a2:       66 90                   xchg   %ax,%axDisassembly of section .fini:00000000004005a4 <_fini>:  4005a4:       48 83 ec 08             sub    $0x8,%rsp  4005a8:       48 83 c4 08             add    $0x8,%rsp  4005ac:       c3                      retq

 

以上结果会根据不同的系统环境而定,我当前的环境是:

[zorro@dhcp-65-110 tmp]$ uname -a
Linux dhcp-65-110.nay.redhat.com 3.12.9-201.fc19.x86_64 #1 SMP Wed Jan 29 15:44:35 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
[zorro@dhcp-65-110 tmp]$ cat /etc/issue
Fedora release 19 (Schrödinger’s Cat)
Kernel \r on an \m (\l)

[zorro@dhcp-65-110 tmp]$ gcc -v
使用内建 specs。
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/lto-wrapper
目标:x86_64-redhat-linux
配置为:../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-isl=/builddir/build/BUILD/gcc-4.8.2-20131212/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.2-20131212/obj-x86_64-redhat-linux/cloog-install --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
线程模型:posix
gcc 版本 4.8.2 20131212 (Red Hat 4.8.2-7) (GCC)


从前面的汇编语言讲解中我们知道.text是代码段,那么上面的汇编程序我们找到.text的起始地址:

Disassembly of section .text:
0000000000400400 <_start>:

正好也是从我们熟悉的_start标号开始。那_start开始后会干什么呢?会调用__libc_start_main

400424:       e8 b7 ff ff ff          callq  4003e0 <__libc_start_main@plt>

__libc_start_main是libc库里的函数,在myprog程序里是看不到实现的,因为它是一个动态链接库,一般会被映射到共享库存储区。但是@plt是一个辅助__libc_start_main的段,这里是先进入.plt段,然后再跳转到实际的__libc_start_main段。

libc_start_main一般会干三件重要的事:

1、初始化程序,执行_init。

2、注册退出处理程序,就是main返回后需要执行的处理程序。

3、调用main函数。


根据我们前面的经验,在函数调用前会先传参,在x86_64下参数一般是由rdi, rcx, r8等寄存器来担任的,如下:

  40040f:       49 c7 c0 a0 05 40 00    mov    $0x4005a0,%r8
  400416:       48 c7 c1 30 05 40 00    mov    $0x400530,%rcx
  40041d:       48 c7 c7 0c 05 40 00    mov    $0x40050c,%rdi
  400424:       e8 b7 ff ff ff          callq  4003e0 <__libc_start_main@plt>

那么__libc_start_main就得到了三个参数,这三个参数是三个地址,在myprog中搜索一下就可以发现这三个地址分别是:

000000000040050c <main>:

0000000000400530 <__libc_csu_init>

00000000004005a0 <__libc_csu_fini>

__libc_csu_init会调用_init,__libc_csu_fini和_fini自然脱不了干系,它是留给程序结束时用的。main函数就是我们最常见的main函数。


这里提到了很多函数符号,入init, fini, libc_start_main, main, foo, bar等。让我们回到链接前的动作,我们只编译出目标文件:

gcc -c myprog.c -o myprog.o

然后我们用objdump来看一下myprog.o的内容,我们发现里面只有main, foo, bar三个标记以及实现。那上面我们讨论的这些从哪来呢? 现在我们想办法把myprog.o链接成myprog可执行文件,使用最原始的链接命令ld来完成。如果你执行ld myprog.o -o myprog, 那么你肯定不会成功,就算成功了你也执行不了。正确的链接方法(在我的系统上,不同的环境会有差异)是:

ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o myprog.o -o myprog -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2


我们看到除了myprog.o文件外我们还另外链接了/usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o三个目标文件,那么这三个目标文件是什么呢?我们用伟大的黑客工具objdump来看一下这三个文件的内容,发现在crt1.o里有_start标号和它的实现,在crti.o和crtn.o中有init和fini的标号和实现。在crt1.o的_start里调用

  24:   e8 00 00 00 00          callq  29 <_start+0x29>

这个调用地址是一个未链接的地址,经过链接后就会变成像__libc_start_main@plt这样的地址。


像libc里的函数是再运行时动态链接的,所以我们用-lc指定了使用libc共享库,并使用-dynamic-linker /lib/ld-linux.so.2指定了用什么用动态链接器来在运行时链接libc的函数。


在程序运行时内容的分布大概是这样的:

+----------------------------------+
|  内核虚拟内存                     |
+----------------------------------+
|  用户栈(运行时创建)     |
+----------------------------------+
|  .......     ..............    .......        |
+----------------------------------+
|  共享库内存映射区域         |
+----------------------------------+
|  ......        .......... ..........          |
+----------------------------------+
|  运行时堆空间                     |
+----------------------------------+
|  读/写段(.data, .bss等)       |
+----------------------------------+
|  只读段(.init. .text等)           |
+----------------------------------+
|  ......      ......            ..........     |
+----------------------------------+


现在看开始的两个问题你心中是否有些许答案? 因为程序执行是一个很大的概念,涉及到进程、虚拟内存、内存映射、链接器和加载器等很大的概念,上面说了那么多也只是冰山一角。所以我也不好在这里都讲清楚,如果后面有时间我会掰开了一一说说。那么我们来概要说一下程序执行:

Linux系统中每个程序都运行在一个进程的上下文中,都有自己的虚拟内存空间。当shell执行一个程序时,它fork出一个子进程,子进程使用execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,创建一组新的段空间分配,并初始化新的栈和堆空间。通过将虚拟内存地址中的页映射到可执行文件的页大小的chunk上,新的代码段和数据段被初始化为可执行文件的内容(实际是写时拷贝)。最后加载器跳转到_start地址开始执行,并最终会调用到应用程序的main函数。

0 0
原创粉丝点击