群里一个朋友问上面的代码是什么原理,为什么这样就可以输出Hello world!了呢?
这段代码奇怪在哪里呢?它居然没有 #include <stdio.h>
等任何标准库的头文件!好吧,不包含头文件,一样可以使用printf()
等函数。头文件的存在不过是告诉编译器去链接哪个符号的,即使没有,编译器会自动链接运行时库来查找这些符号的,只是可能会给出警告而已。然而,细读下去,整段代码居然也没有使用任何标准库的函数。这就比较尴尬了……
标准库和系统调用
为了弄清出其中的原理,首先需要了解C标准库和系统调用之间的关系:不准确地说,C标准库是对系统调用(syscall)的封装,其目的是实现一套跨平台的统一的API。而系统调用(syscall)是操作系统提供的使用户可以访问操作系统内核的一套API。 既然标准库是由系统调用实现的,那么必然存在跨过C标准库直接使用系统调用的方法。但是还有一个阻碍,既然没有标准库(在Linux gcc下为glibc,Windows VC下则为msvcrtXX.dll),动态链接器是怎么查找到系统调用的符号地址的呢?如此,就需要了解可执行文件的载入过程。
编译
->汇编
->链接
->执行
,这是每个学习C的人都耳熟能详的过程。以上代码的编译和汇编阶段毫无疑问是没问题的(虽然可能会有Unused variable
的警告)。问题是,在没有使用任何外部符号(symbol)的情况下,链接过程中,会链接到别的运行库么?我们可以在编译时打开gcc的--verbose
选项显示每个过程的详细信息,或者使用ldd查看生成的二进制程序都链接了哪些运行库。
而在Windows下,Win32程序的在运行时都是要链接到动态运行库ntdll.dll
和kernel32.dll
的。ntdll.dll
和kernel32.dll
是什么呢?
未完待续,先贴个简单的分析
大致过程是:
1. threadEnvironmentBlock()
: 利用线程的FS寄存器寻址找到当前线程的TEB结构
2. processEnvironmentBlock()
: 从TEB结构体中找到线程所在进程的PEB结构
3. ktv()
5-12: 使用PEB结构体中的flink
指针(侵入式链表指针)遍历寻找ntdll.dll
和kernel32.dll
在内存中的地址
4. 13-19: 从kernel32.dll
中寻找符号导出表(EXPORT
)的地址
5. 20-27: 从符号导出表中利用指针偏移寻找getStdHandle()
调用从而找到stdout
这个Handle(在linux下叫做file descriptor);寻找Console()
调用的函数地址
6. 28-32: 使用已经寻找到的Console()
调用向stdout输出”hello world!\n”
参考:
TEB/PEB/PEB_LDR_DATA
https://msdn.microsoft.com/zh-cn/aa813708(v=vs.85))
http://www.weixianmanbu.com/article/76.html
http://www.cnblogs.com/dsky/archive/2012/02/20/2358864.html
http://www.nirsoft.net/kernel_struct/vista/peb.html
http://www.nirsoft.net/kernel_struct/vista/TEB.html
http://www.nirsoft.net/kernel_struct/vista/PEB_LDR_DATA.html
GCC内联汇编
https://linux.cn/article-7688-1.html
LIST_ENTRY (侵入式链表)
https://linux.cn/article-7321-1.html