接着说“调用”,前面我们提到了一点系统调用,又略微详细的说了一下函数调用,现在接着说一下程序调用,应该说是执行一个可执行程序。
前面的几篇文章都在围绕着函数调用栈来说,从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-64 Disassembly 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,%ax Disassembly 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函数。