C语言hello world的源码:
#include <stdio.h>
int main() {
printf("hello world!\n");
return 0;
}
源码还是很简单的,本文主要探讨这段代码执行过程。
储存
这样看似简单的一段代码在电脑中是怎么存储的?
现在计算机系统使用ASCII标准来表示文本字符。
使用gcc编译源码,得到一个可执行文件,查看这个文件。
首先关注到这个ELF,它表明了Linux中编译的C语言程序是ELF格式的,而在windows中去PE的格式。一个可执行的二进制文件包含的不仅仅是机器指令,还包括各种数据、程序运行资源,机器指令只是其中的一部分。
Linux可执行文件格式-ELF结构详解 这篇博客讲解的很详细
我们这个代码的输出毋庸置疑的是输出的Hello world!,那么有没有一种办法不修改源码,改变输出的结果呢,答案是肯定的,我们可以在可执行文件操作。
在可执行文件中可以找到这样一行,这里的h,右边输出了关于h的信息,这里的68是16进制数,它的ascll值是104,那么就可以修改这里从而修改程序的运行结果
通过这种方式应该是可以写出游戏外挂的吧,不过出错的概率太大了。
这也可以说明Linux中一切都是文件。
生成
在生成可执行文件的过程又发生了什么
还是这个4个阶段。
预处理
这个过程主要根据以“#”开头的预编译,修改原始的C程序。
gcc -E hello.c -o hello.i
这里将#incldue<stdio.h>替换成了上面的代码。
编译
编译器将文本文件hello.i进行语法分析编译优化等生成hello.s。生成相应的汇编代码。
gcc -S hello.i -o hello.s
生成的汇编代码
.file "hello.c" ; 指定源文件的名称,用于调试信息
.text ; 指定代码段的开始,代码段用于存放指令
.section .rodata ; 指定只读数据段的开始,只读数据段用于存放常量
.LC0: ; 定义一个标签,用于标记一个地址,方便后面引用
.string "hello world!" ; 定义一个字符串常量,以$结尾
.text ; 重新指定代码段的开始
.globl main ; 声明main函数是一个全局符号,可以被其他模块引用
.type main, @function ; 声明main函数的类型是函数
main: ; 定义main函数的标签,表示函数的入口地址
.LFB0: ; 定义一个内部标签,用于调试信息
.cfi_startproc ; 指定函数的开始,用于生成调用帧信息
endbr64 ; 一个特殊的指令,用于防止恶意代码的跳转
pushq %rbp ; 将寄存器rbp的值压入栈中,保存原来的rbp值
.cfi_def_cfa_offset 16 ; 指定当前栈帧的偏移量为16字节,用于调用帧信息
.cfi_offset 6, -16 ; 指定寄存器rbp在栈中的偏移量为-16字节,用于调用帧信息
movq %rsp, %rbp ; 将寄存器rsp的值赋给寄存器rbp,设置当前的栈帧基址
.cfi_def_cfa_register 6 ; 指定寄存器rbp是当前栈帧的基址,用于调用帧信息
leaq .LC0(%rip), %rax ; 将标签.LC0的地址加上寄存器rip的值,赋给寄存器rax,rip是指令指针,表示当前指令的地址
movq %rax, %rdi ; 将寄存器rax的值赋给寄存器rdi,rdi是第一个函数参数的传递寄存器,这里是将字符串的地址作为参数传递
call puts@PLT ; 调用puts函数,输出字符串,@PLT是一个链接器的技术,用于解析函数的实际地址
movl $0, %eax ; 将立即数0赋给寄存器eax,eax是函数返回值的寄存器,这里是将0作为返回值
popq %rbp ; 将栈顶的值弹出,赋给寄存器rbp,恢复原来的rbp值
.cfi_def_cfa 7, 8 ; 指定当前栈帧的基址和偏移量,用于调用帧信息
ret ; 返回指令,从函数中返回,跳转到返回地址
.cfi_endproc ; 指定函数的结束,用于生成调用帧信息
.LFE0: ; 定义一个内部标签,用于调试信息
.size main, .-main ; 指定main函数的大小,用于调试信息
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" ; 指定编译器的版本信息
.section .note.GNU-stack,"",@progbits ; 指定一个特殊的段,用于告诉链接器栈的属性
.section .note.gnu.property,"a" ; 指定一个特殊的段,用于告诉链接器一些属性信息
.align 8 ; 指定对齐为8字节
.long 1f - 0f ; 定义一个长整数,值为两个标签之间的距离
.long 4f - 1f ; 定义一个长整数,值为两个标签之间的距离
.long 5 ; 定义一个长整数,值为5
0: ; 定义一个标签
.string "GNU" ; 定义一个字符串,表示属性的名称
1: ; 定义一个标签
.align 8 ; 指定对齐为8字节
.long 0xc0000002 ; 定义一个长整数,表示属性的类型
.long 3f - 2f ; 定义一个长整数,表示属性的大小
2: ; 定义一个标签
.long 0x3 ; 定义一个长整数,表示属性的值
3: ; 定义一个标签
.align 8 ; 指定对齐为8字节
4: ; 定义一个标签
汇编
汇编器将hello.s翻译成机器指令,并将这些指令打包成可重定向目标程序,并将结果保存在hello.o,hello.o是一个可执行文件。
gcc -c hello.s -o hello.o
既然是一个二进制文件那么这个文件就可以执行了吗?
这个时候的二进制文件就只包含了函数main的指令编码。
链接
在hello程序中调用了printf,而这个函数是由C编译器提供的,存在一个叫做printf.o的程序中,我们需要将这个文件和我们的hello.o结合在一起,链接器的工作就是 这样,结果得到一个hello的可执行文件。
gcc hello.o -o hello
运行
hello.c 程序已经被编译可执行的目标文件 hello,且存在磁盘上。那这个程序是如何运行起来的呢?
我们是在终端执行的hello,同时终端也就是shell同样也是一个程序,当我们从键盘输入./hello,并按下回车时,hello程序执行并输出结果到显示器。
这里就涉及两个进程,一个shell一个hello,在一个系统中可以同时运行多个进程,而每一个进程都好像独占使用硬件,这就是并发运行,操作系统实现这种机制叫做上下文切换。
最开始shell进程等待命令行输入,当运行hello程序,shell程序调用系统调用,系统调用就会将控制权传递给操作系统,操作系统保存shell的上下文,创建hello的上下文,将控制权给hello,当hello进程结束,操作系统回复shell进程上下文,继续等待下一条指令。
shell程序在接收到./hello后,将字符逐一读入寄存器 ,再将它存入内存中
当回车时,shell执行一系列的指令,将hello目标文件的代码和数据从磁盘拷贝到主存,加载hello文件(使用DMA技术)。
在 Linux 系统中,每个程序都有一个运行时的内存映像。
- 程序代码和数据:这里解释hello
- 堆:malloc,new等在程序运行时动态扩展
- 共享库:这里放的就是printf函数
- 栈:编译器用这个实现函数调用
- 内核虚拟内存:保留区域
当hello目标文件加载到存储器,处理器开始执行hello程序中的机器语言指令,这些指令将输出结果中的字节从储存器拷贝到寄存器,再从寄存器拷贝到显示设备
源码解析
嗯,这程序也需要源码解析?
main
首先我们都知道函数的运行入口是main,但是为什么是main,可以不可以是别的。
其实是main函数最多还是一种默认的规定,在没有操作系统的环境来说任何都可以函数的入口,毕竟都是自己定义的,main函数和其他的任何函数都没有本质区别
从汇编代码来看{
都是创建相应的栈帧,将基地址指针的至压入栈中,再将栈指针的地址赋值给基地址指针
}
就是返回当前栈帧
也就是正括号用来保护上层函数的栈帧,并设置当前函数的栈帧
反括号用来放弃当前函数的栈帧
gcc main.o -o main
是调用 ld 来做链接的,相当于以下的命令:
ld /usr/lib64/crt1.o /usr/lib64/crti.o main.o -o main -lc -dynamic-linker /lib/ld-linux.so.2
主要是crt1.o,crti.o这两个文件,crt1.o和crti.o是C语言程序的启动模块,它们是由C库提供的目标文件,用于在调用main函数之前做一些初始化工作。它们通常会被自动链接到应用程序中,但也可以通过编译选项来指定或忽略它们
crt1.o文件中包含了程序的入口函数start,以及两个未定义的符号_libc_start_main和main。_start函数会负责调用__libc_start_main函数来初始化libc,然后调用我们源代码中定义的main函数。_start函数也会把程序的执行状态以整数的方式传递给操作系统
crti.o文件中包含了.init和.fini两个段,它们分别用于执行初始化函数init()和终止函数fini()。.init段中包含了进程的初始化代码,即当程序开始执行时,系统会在调用main函数之前先执行.init中的代码。.fini段中包含了进程的终止代码,即当程序正常退出时(main函数返回之后),系统会安排执行.fini中的代码
crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o参考文章
整个可执行程序真正的入口点是 crt1.o 中的 _start,而 main 函数是被 _start 调用的
#include <stdio.h>
void _start() {
printf("Hello, World!\n");
exit(0);
}
直接使用gcc -o main main.c
编译,编译失败!
这里应该加上-nostartfiles
,告诉编译器:我要使用自己的入口!
再次编译,仍然出错
这个警告的原因是没有添加#include <stdlib.h>这个头文件,这个头文件包含了exit,如果没有添加编译器只能自己推导。
编译完成同样可以成功输出结果。
反汇编
两种方式的反汇编对比
_start:
Disassembly of section .plt:
0000000000400360 <.plt>:
400360: ff 35 a2 0c 20 00 pushq 0x200ca2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400366: ff 25 a4 0c 20 00 jmpq *0x200ca4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40036c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400370 <puts@plt>:
400370: ff 25 a2 0c 20 00 jmpq *0x200ca2(%rip) # 601018 <puts@GLIBC_2.2.5>
400376: 68 00 00 00 00 pushq $0x0
40037b: e9 e0 ff ff ff jmpq 400360 <.plt>
0000000000400380 <exit@plt>:
400380: ff 25 9a 0c 20 00 jmpq *0x200c9a(%rip) # 601020 <exit@GLIBC_2.2.5>
400386: 68 01 00 00 00 pushq $0x1
40038b: e9 d0 ff ff ff jmpq 400360 <.plt>
Disassembly of section .text:
0000000000400390 <_start>:
400390: 55 push %rbp
400391: 48 89 e5 mov %rsp,%rbp
400394: bf a8 03 40 00 mov $0x4003a8,%edi
400399: e8 d2 ff ff ff callq 400370 <puts@plt>
40039e: bf 00 00 00 00 mov $0x0,%edi
4003a3: e8 d8 ff ff ff callq 400380 <exit@plt>
main:
hello: file format elf64-x86-64
Disassembly of section .plt:
0000000000400360 <.plt>:
400360: ff 35 a2 0c 20 00 pushq 0x200ca2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400366: ff 25 a4 0c 20 00 jmpq *0x200ca4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40036c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400370 <puts@plt>:
400370: ff 25 a2 0c 20 00 jmpq *0x200ca2(%rip) # 601018 <puts@GLIBC_2.2.5>
400376: 68 00 00 00 00 pushq $0x0
40037b: e9 e0 ff ff ff jmpq 400360 <.plt>
0000000000400380 <exit@plt>:
400380: ff 25 9a 0c 20 00 jmpq *0x200c9a(%rip) # 601020 <exit@GLIBC_2.2.5>
400386: 68 01 00 00 00 pushq $0x1
40038b: e9 d0 ff ff ff jmpq 400360 <.plt>
Disassembly of section .text:
0000000000400390 <_start>:
400390: 55 push %rbp
400391: 48 89 e5 mov %rsp,%rbp
400394: bf a8 03 40 00 mov $0x4003a8,%edi
400399: e8 d2 ff ff ff callq 400370 <puts@plt>
40039e: bf 00 00 00 00 mov $0x0,%edi
4003a3: e8 d8 ff ff ff callq 400380 <exit@plt>
[root@VM-4-4-centos hello]# gcc -o hello hello.c
[root@VM-4-4-centos hello]# objdump -S hello
hello: file format elf64-x86-64
Disassembly of section .init:
00000000004003e0 <_init>:
4003e0: 48 83 ec 08 sub $0x8,%rsp
4003e4: 48 8b 05 0d 0c 20 00 mov 0x200c0d(%rip),%rax # 600ff8 <__gmon_start__>
4003eb: 48 85 c0 test %rax,%rax
4003ee: 74 05 je 4003f5 <_init+0x15>
4003f0: e8 3b 00 00 00 callq 400430 <__gmon_start__@plt>
4003f5: 48 83 c4 08 add $0x8,%rsp
4003f9: c3 retq
Disassembly of section .plt:
0000000000400400 <.plt>:
400400: ff 35 02 0c 20 00 pushq 0x200c02(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400406: ff 25 04 0c 20 00 jmpq *0x200c04(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40040c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400410 <puts@plt>:
400410: ff 25 02 0c 20 00 jmpq *0x200c02(%rip) # 601018 <puts@GLIBC_2.2.5>
400416: 68 00 00 00 00 pushq $0x0
40041b: e9 e0 ff ff ff jmpq 400400 <.plt>
0000000000400420 <__libc_start_main@plt>:
400420: ff 25 fa 0b 20 00 jmpq *0x200bfa(%rip) # 601020 <__libc_start_main@GLIBC_2.2.5>
400426: 68 01 00 00 00 pushq $0x1
40042b: e9 d0 ff ff ff jmpq 400400 <.plt>
0000000000400430 <__gmon_start__@plt>:
400430: ff 25 f2 0b 20 00 jmpq *0x200bf2(%rip) # 601028 <__gmon_start__>
400436: 68 02 00 00 00 pushq $0x2
40043b: e9 c0 ff ff ff jmpq 400400 <.plt>
Disassembly of section .text:
0000000000400440 <_start>:
400440: 31 ed xor %ebp,%ebp
400442: 49 89 d1 mov %rdx,%r9
400445: 5e pop %rsi
400446: 48 89 e2 mov %rsp,%rdx
400449: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40044d: 50 push %rax
40044e: 54 push %rsp
40044f: 49 c7 c0 c0 05 40 00 mov $0x4005c0,%r8
400456: 48 c7 c1 50 05 40 00 mov $0x400550,%rcx
40045d: 48 c7 c7 2d 05 40 00 mov $0x40052d,%rdi
400464: e8 b7 ff ff ff callq 400420 <__libc_start_main@plt>
400469: f4 hlt
40046a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0000000000400470 <deregister_tm_clones>:
400470: b8 3f 10 60 00 mov $0x60103f,%eax
400475: 55 push %rbp
400476: 48 2d 38 10 60 00 sub $0x601038,%rax
40047c: 48 83 f8 0e cmp $0xe,%rax
400480: 48 89 e5 mov %rsp,%rbp
400483: 77 02 ja 400487 <deregister_tm_clones+0x17>
400485: 5d pop %rbp
400486: c3 retq
400487: b8 00 00 00 00 mov $0x0,%eax
40048c: 48 85 c0 test %rax,%rax
40048f: 74 f4 je 400485 <deregister_tm_clones+0x15>
400491: 5d pop %rbp
400492: bf 38 10 60 00 mov $0x601038,%edi
400497: ff e0 jmpq *%rax
400499: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004a0 <register_tm_clones>:
4004a0: b8 38 10 60 00 mov $0x601038,%eax
4004a5: 55 push %rbp
4004a6: 48 2d 38 10 60 00 sub $0x601038,%rax
4004ac: 48 c1 f8 03 sar $0x3,%rax
4004b0: 48 89 e5 mov %rsp,%rbp
4004b3: 48 89 c2 mov %rax,%rdx
4004b6: 48 c1 ea 3f shr $0x3f,%rdx
4004ba: 48 01 d0 add %rdx,%rax
4004bd: 48 d1 f8 sar %rax
4004c0: 75 02 jne 4004c4 <register_tm_clones+0x24>
4004c2: 5d pop %rbp
4004c3: c3 retq
4004c4: ba 00 00 00 00 mov $0x0,%edx
4004c9: 48 85 d2 test %rdx,%rdx
4004cc: 74 f4 je 4004c2 <register_tm_clones+0x22>
4004ce: 5d pop %rbp
4004cf: 48 89 c6 mov %rax,%rsi
4004d2: bf 38 10 60 00 mov $0x601038,%edi
4004d7: ff e2 jmpq *%rdx
4004d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000004004e0 <__do_global_dtors_aux>:
4004e0: 80 3d 4d 0b 20 00 00 cmpb $0x0,0x200b4d(%rip) # 601034 <_edata>
4004e7: 75 11 jne 4004fa <__do_global_dtors_aux+0x1a>
4004e9: 55 push %rbp
4004ea: 48 89 e5 mov %rsp,%rbp
4004ed: e8 7e ff ff ff callq 400470 <deregister_tm_clones>
4004f2: 5d pop %rbp
4004f3: c6 05 3a 0b 20 00 01 movb $0x1,0x200b3a(%rip) # 601034 <_edata>
4004fa: f3 c3 repz retq
4004fc: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400500 <frame_dummy>:
400500: 48 83 3d 18 09 20 00 cmpq $0x0,0x200918(%rip) # 600e20 <__JCR_END__>
400507: 00
400508: 74 1e je 400528 <frame_dummy+0x28>
40050a: b8 00 00 00 00 mov $0x0,%eax
40050f: 48 85 c0 test %rax,%rax
400512: 74 14 je 400528 <frame_dummy+0x28>
400514: 55 push %rbp
400515: bf 20 0e 60 00 mov $0x600e20,%edi
40051a: 48 89 e5 mov %rsp,%rbp
40051d: ff d0 callq *%rax
40051f: 5d pop %rbp
400520: e9 7b ff ff ff jmpq 4004a0 <register_tm_clones>
400525: 0f 1f 00 nopl (%rax)
400528: e9 73 ff ff ff jmpq 4004a0 <register_tm_clones>
000000000040052d <main>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: bf e0 05 40 00 mov $0x4005e0,%edi
400536: e8 d5 fe ff ff callq 400410 <puts@plt>
40053b: b8 00 00 00 00 mov $0x0,%eax
400540: 5d pop %rbp
400541: c3 retq
400542: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400549: 00 00 00
40054c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400550 <__libc_csu_init>:
400550: 41 57 push %r15
400552: 41 89 ff mov %edi,%r15d
400555: 41 56 push %r14
400557: 49 89 f6 mov %rsi,%r14
40055a: 41 55 push %r13
40055c: 49 89 d5 mov %rdx,%r13
40055f: 41 54 push %r12
400561: 4c 8d 25 a8 08 20 00 lea 0x2008a8(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
400568: 55 push %rbp
400569: 48 8d 2d a8 08 20 00 lea 0x2008a8(%rip),%rbp # 600e18 <__init_array_end>
400570: 53 push %rbx
400571: 4c 29 e5 sub %r12,%rbp
400574: 31 db xor %ebx,%ebx
400576: 48 c1 fd 03 sar $0x3,%rbp
40057a: 48 83 ec 08 sub $0x8,%rsp
40057e: e8 5d fe ff ff callq 4003e0 <_init>
400583: 48 85 ed test %rbp,%rbp
400586: 74 1e je 4005a6 <__libc_csu_init+0x56>
400588: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40058f: 00
400590: 4c 89 ea mov %r13,%rdx
400593: 4c 89 f6 mov %r14,%rsi
400596: 44 89 ff mov %r15d,%edi
400599: 41 ff 14 dc callq *(%r12,%rbx,8)
40059d: 48 83 c3 01 add $0x1,%rbx
4005a1: 48 39 eb cmp %rbp,%rbx
4005a4: 75 ea jne 400590 <__libc_csu_init+0x40>
4005a6: 48 83 c4 08 add $0x8,%rsp
4005aa: 5b pop %rbx
4005ab: 5d pop %rbp
4005ac: 41 5c pop %r12
4005ae: 41 5d pop %r13
4005b0: 41 5e pop %r14
4005b2: 41 5f pop %r15
4005b4: c3 retq
4005b5: 90 nop
4005b6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005bd: 00 00 00
00000000004005c0 <__libc_csu_fini>:
4005c0: f3 c3 repz retq
Disassembly of section .fini:
00000000004005c4 <_fini>:
4005c4: 48 83 ec 08 sub $0x8,%rsp
4005c8: 48 83 c4 08 add $0x8,%rsp
4005cc: c3 retq
可以看出使用_start函数的反汇编代码相较于main函数的还是要简短很多,这中间的过程就是链接的过程
crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o参考文章
printf
printf做的工作之一就是系统调用,printf必须要做的系统调用就是write,因此代码可以直接替换如下
_start() {
write(1, "Hello World\n", 12);
exit(0);
}
这样一段没有头文件,没有main函数的C程序是可以正常运行的
使用内联汇编实现
_start()
{
char* msg = "Hello World!";
int len = 11;
int result = 0;
__asm__ __volatile__("movl %2, %%edx;\n\r" // 传入参数:要显示的字符串长度
"movl %1, %%ecx;\n\r" //传入参赛:文件描述符(stdout
"movl $1, %%ebx;\n\r" //传入参数:要显示的字符串
"movl $4, %%eax;\n\r" //系统调用号:4 sys_write
"int $0x80" //触发系统调用中断
:"=m"(result) //输出部分:本例并未使用
:"m"(msg),"r"(len) // 输入部分:绑定字符串和字符串长度变量
:"%eax");
exit(0);
}
这里的__volatile__说明不要编译器优化
当对代码进行O2优化,编译器可能自动改变改变我们的代码(汇编层面的)
测试代码:
int a = 0;
volatile int b = 0;
int nouse_vol() {
while(a > 1) {
}
return 0;
}
int use_vol(){
while(b > 1) {
}
return 0;
}
汇编的O2优化:
nouse_vol():
xor eax, eax
ret
use_vol():
.L4:
mov eax, DWORD PTR b[rip]
cmp eax, 1
jg .L4
xor eax, eax
ret
b:
.zero 4
a:
.zero 4
可以看到在O2优化下,在不使用volatile的情况下,编译器默认就没有执行while的汇编代码
使用汇编实现
section .text
global _start
_start:
mov rdi, 1 ;arg0
mov rdx, 8 ;arg2
mov rbx, 0x0a646c726f576f6c6c6548 ;arg1 = Hello World\n
push rbx
mov rsi, rsp
mov rax, 1
syscall
mov rax, 60 ;exit
syscall