从一个hello world说起
大家好,我是明说网络的小明同学。今天我们从C语言的Hello World说起,和大家一起温习一下C语言中一个Hello World怎么运行起来的,以及C语言如何组织栈缓冲区等。本文不适用于C语言初学者,需要具备有一定的汇编基础。好了下面,我们开始吧。
工具本文的工具为:
操作系统:Ubuntu16.04, 4.15.0-142-generic编译器:gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)make工具GNU Make 4.1反汇编查看器:objdumpelf文件查看器:readelf
C语言介绍C 语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验室为开发 UNIX 操作系统而设计的。UNIX 操作系统,C编译器,和几乎所有的 UNIX 应用程序都是用 C 语言编写的。由于各种原因,C 语言现在已经成为一种广泛使用的专业语言。
同时,C语言是一门大学期间基本上都会开设的课程。作为一门入门编程课程,C语言有着独特的魅力和不可替代的作用。虽然当前python火热,C语言好像显得不那么重要了,“python难道不香吗”的疑问开始出现。但是我的观点是:每种语言有每种语言的优势,python永远也取代不了C语言。像我独爱指针,能够带来自由的感觉。
下面就开始我们的探索之旅吧。
第一个程序helloworld编写程序首先我们有如下程序:main.c
//main.c#include上述程序实现的功能很简单,就是输出一句话hello world! I'm a string,为了便于说明,其中故意使用了一个函数调用int display(char *)。
函数的逻辑为,main函数--> display()函数(一个参数)-->printf函数(两个参数)。
是不是很简单!
程序编译makefile为了便于说明,我们使用makefile文件进行编译。创建文件名为makefile的文件,内容如下:
# makefileOBJ=printf.main$(OBJ):gcc main.c -o $@clean:-rm $(OBJ)我们生成的文件名为printf.main,这里你可以改为你喜欢的任意名称。
使用make命令进行编译,会生成最终文件。运行后就可以看见hello world! I'm a string
小结到这里我们就完成了一个helloworld程序的编写和编译,并且运行。是不是很简单。对于初学者,其实到这里就完了,姑且可以认为main函数就是一个程序的开始和结束(我曾经就一直这么认为)。但是对于有过一定经验的人来说,就知道:main函数并不是一个程序的开始,也不是一个程序的结束。
咦,这么神奇的吗?就让我们来看看吧。
Hello world 的背后首先让我们来认识一下我们生成的printf.main。
file ./printf.main ./printf.main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5c389a402866aaa012b8b8ab992fed778eb989b0, not strippedELF是执行和链接格式(Execurable and Linking Format)的缩略词。它是UNIX系统的几种可执行文件格式中的一种。
使用命令readelf -h ./printf.main > elf_head.txt
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: EXEC (Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x4004a0 //注意这一行Start of program headers: 64 (bytes into file)Start of section headers: 6712 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 9Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 28这里面,我们注意第11行,Entry point address: 0x4004a0,显示,入口点地址为address,说明操作系统在运行这个printf.main程序时,首先从这个地址开始运行。那么我们看看这个地址到底是什么吧
汇编使用命令objdump -d printf.main > objdump.txt将程序的汇编代码提取出来(删除了一些当前没有必要说明的内容),如下所示:
printf.main: file format elf64-x86-64Disassembly of section .init:0000000000400428 <_init>:400428: 48 83 ec 08 sub $0x8,%rsp40042c: 48 8b 05 c5 0b 20 00 mov 0x200bc5(%rip),%rax # 600ff8 <_DYNAMIC+0x1d0>400433: 48 85 c0 test %rax,%rax400436: 74 05 je 40043d <_init+0x15>400438: e8 53 00 00 00 callq 400490 <__libc_start_main@plt+0x10>40043d: 48 83 c4 08 add $0x8,%rsp400441: c3 retq Disassembly of section .plt:0000000000400470这里我们注意第45,46,47,48行,注意其中
4004af: 49 c7 c0 b0 06 40 00 mov $0x4006b0,%r8 //00000000004006b0 <__libc_csu_fini>:4004b6: 48 c7 c1 40 06 40 00 mov $0x400640,%rcx //0000000000400640 <__libc_csu_init>:4004bd: 48 c7 c7 bb 05 40 00 mov $0x4005bb,%rdi //00000000004005bb__libc_start_main@plt包含了三个参数,__libc_csu_fini,__libc_csu_init,main显然,从名称上就可以看出这四个函数的作用。
__libc_start_main是libc.so.6中的一个函数。它的原型是这样的:
extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),int argc,char *__unbounded *__unbounded ubp_av,void (*init) (void),void (*fini) (void),void (*rtld_fini) (void),void *__unbounded stack_end)__attribute__ ((noreturn));这个函数需要做的是建立/初始化一些数据结构/环境然后调用我们的main()。
程序启动的过程应该:_start -> __libc_start_main -> __libc_csu_init -> _init -> main -> _fini.
这篇文章有详细的说明:linux编程之main()函数启动过程
栈缓冲区及结构x86_64有16个64位寄存器,分别是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。其中:
%rax 作为函数返回值使用。 %rsp 栈指针寄存器,指向栈顶 %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数 %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改 %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
64位与32位的不同在于64位不用压栈来存储下一个函数参数,而是放在了%rdi,%rsi,%rdx,%rcx,%r8,%r9六个寄存器中,超出部分再压栈。
首先,我们将main.c文件进行汇编,使用命令gcc -S main.c,在当前目录下会生成main.s的汇编文件,内容如下:
.file "main.c".section .rodata.LC0:.string "hello world! %sn".text.globl display.type display, @functiondisplay:.LFB0:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovq %rdi, -8(%rbp)movq -8(%rbp), %raxmovq %rax, %rsimovl $.LC0, %edimovl $0, %eaxcall printfnopleave.cfi_def_cfa 7, 8ret.cfi_endproc.LFE0:.size display, .-display.globl main.type main, @functionmain:.LFB1:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $272, %rspmovq %fs:40, %raxmovq %rax, -8(%rbp)xorl %eax, %eaxmovabsq $8391086132249306953, %rax //0x74732061206d2749 ("I'm a st")movq %rax, -272(%rbp)movq $1735289202, -264(%rbp)leaq -256(%rbp), %rdxmovl $0, %eaxmovl $30, %ecxmovq %rdx, %rdirep stosqleaq -272(%rbp), %raxmovq %rax, %rdi //使用%rdi寄存器压入参数call display //调用函数movl $0, %eaxmovq -8(%rbp), %rsixorq %fs:40, %rsije .L4call __stack_chk_fail.L4:leave.cfi_def_cfa 7, 8ret.cfi_endproc.LFE1:.size main, .-main.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609".section .note.GNU-stack,"",@progbitsmain函数在53,54行,使用rdi压入了一个参数,参数的地址在-272(%rbp)(即rdi), 可以看出正好是字符串"I'm a string"的地址。如下所示:
其中,函数调用栈缓冲区backtrace显示当前栈缓冲区为main,再上一层为__libc_start_main,再次印证了上一节的说法。
display函数下面我们进入display函数,可以看出printf的两个参数分别放在rdi,rsi两个寄存器当中。
其中,函数调用栈缓冲区backtrace显示当前栈缓冲区为display,再上一层为main,__libc_start_main,再次印证了上一节的说法。
小结通过对main函数中display函数的参数,display函数中的printf函数的参数进行实验,说明了C语言在函数调用时的栈缓冲区的组织。
结语对于一个普普通通的C语言程序,其实其背后是一堆复杂的操作系统预备好的操作,执行完毕之后,就开始执行我们的main函数。main函数并不是程序执行的第一个函数,当然也不是最后一个。我们编写的程序的main函数,仅仅是操作系统在加载elf文件时候调用的函数而已,仅仅是函数而已。
栈缓冲区的组织,一定要动手自己调一调,理解栈缓冲区,有助于理解pwn题中的栈缓冲的利用。
这就是我喜欢C语言的原因,因为他能让我更加清晰地看到程序运行的背后,而像python这类语言,我也使用,因为真的方便,但是对于理解计算机、理解背后的故事非常的不利。
关注我,学习更多系统的知识!