上接:
ESP:(extended stack pointer,栈指针寄存器);EBP:(extended base pointer,基址指针寄存器)
glibc:(GNU C library,是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。
MSVC:微软(MS)的VC运行库。
MSVC CRT:Microsoft Visual C++ C Runtime Library,是Windows下的C运行库。
TLS:(Thread Local Storage,线程局部存储)
SDK:(software developmet kit,软件开发包)
堆空间并不是向上增长的,这是源于类unix系统使用类似brk()
的方法分配堆空间,brk()
是向上分配的。但是windows的HeapCreate()
就不是。
大小端:
segment fault
和 非法操作,该内存地址不能read/write
是典型的非法指针解引用造成的错误。栈:
ebp-4
相关地址访问参数)ebp
压栈。(保存上一次的ebp,以供函数返回时修正栈帧)esp
同时回收函数局部空间 。esp
都将发生相应变化 )mov edi edi
、nop
这种占位符的存在。用于函数在运行时可以被其他函数替换掉, 通过将其替换成对其他函数的jmp
。调用惯例:
cdecl
调用惯例。(参数从右到左压栈、由调用方出栈、名字修饰在函数前加_
)this
指针存放与ecx
寄存器传递 。而gcc、thiscall、cedcl只是将this
看作函数的第一个参数传递。函数返回值的传递:
eax
寄存器传递,5~8字节由eax
、edx
寄存器组合传递。eax
将这个空间的地址作为返回值传递出去,函数返回后,调用者将eax
指向的临时对象再拷贝给需要的地方。造成返回值对象会被拷贝两次。brk()
、 mmap()
。
mmap()
分配一块匿名空间,在匿名空间中为用户分配空间。brk()
:用于设置进程的数据段介结束地址。 Linux将数据段和BSS合并为数据段,将数据段结束地址扩大,数据段中扩大的部分就可以作为堆空间之一。 int brk(void* end_data_segment)
mmap()
:向OS申请一段虚拟地址空间,这个空间可以映射到某个文件(这个系统调用最初的作用),将这个空间不映射文件时,就是匿名空间,用于作为堆空间。void *mmap(void* start,size_t length, int prot,int falgs,int fd,off_t offset)
(需要设置起始、长度、权限、映射类型(文件/匿名空间)、文件描述符、文件偏移)VirtualAlloc()
API 申请空间,要求申请的空间需是页的整数倍。HeapCreate
、分配HeapAlloc
、释放HeapFree
和销毁HeapDestroy
堆空间的API。
NTDLL.DLL
和Ntoskrnl.exe
都有一份堆管理器,前者负责Windows子系统DLL与windows内核之间的接口,所有应用程序、运行时库、子系统的堆分配都是使用这部分代码;后者负责内核中堆空间的分配。入口函数:
atexit
注册的函数,然后结束进程。(入口函数通常给运行库的一部分)
入口函数实现: glibc & MSVC
_start
,由ld
链接器默认的连接脚本指定。
env
、argv
、argc
。(环境变量包括系统搜索路径、OS版本等。)_start
最终调用_libc_start_main
,并传入main
、argv
、argc
、init
(初始化函数)、fini
(收尾工作)、rtld_fini
(动态链接收尾工作)、栈底指针。_libc_start_main
:保存env
的栈中地址、栈底地址。
_libc_start_main
首先调用了一系列初始化函数和atexit
注册main
之后的函数。_libc_start_main
的末尾调用了 main
函数 ,在main
后面跟着 exit
函数 。
exit
函数while 循环执行atexit
和__cxa_atexit
注册的函数链表。exit
函数退出。
main
函数正常退出,exit
函数也会被调用。exit
是进程正常退出的必经之路,因此将atexit
注册的函数任务交给exit
函数执行则万无一失。mianCRTStartup
。
malloc
分配内存,则使用alloc
动态的分配栈上内存。(最终还是调整esp释放这些栈上内存)main
函数argv
、设置环境变量、配置其他C库。main
函数,然后except
负责最后的清理,最终返回main
的返回值。运行库与I/O:
stdin
、stdout
、stderr
及其对应的FILE
结构,使得main
之后可以直接使用printf
、scanf
等函数。在OS层面上,文件操作也有类似FILE的概念,Linux中的fd(file descriptor,文件描述符)、windows中的句柄(Handle)
用户通过函数打开文件获得句柄,随后通过句柄操作文件。
句柄:用于防止用户随意读写OS内核的文件对象,文件句柄总和内核的文件对象相关联。
Linux中的fd,0、1、2代表了:标准输入、标准输入、标准错误输出。
MSVC CRT的入口函数初始化:
HeapCreate
API创建了一个系统堆。HeapAlloc
API,将堆的管理交给了OS。(不是由运行库进行堆的管理?)_file
,是一个整数,通过_file
访问内部文件句柄表的项。
ioinfo
数据结构来表示。(包含打开文件的句柄、文件打开属性、管道的单字符缓冲)crt\src\ioinit.c
中有个二维数组__pioinfo
表示用户态的打开文件表。
_file
字段中的0~ 4bits和5 ~ 10bits来表示:用户态打开文件表的二维坐标。C语言运行库:C语言的运行时库
C语言标准库:
glibc与MSVC CRT:
c语言运行库像是C语言程序与不同OS之间的抽象层,将不同OS 的API抽象成相同库函数。
pthread_create
、MSVCRT有_beginthread
来创建线程。glibc:
libc.a
、动态libc.so.6
两个版本。ctr1.o
、ctri.o
、ctrn.o
。
ctr1.o
中包含了_start
程序入口函数,它负责调用__libc_start_main()
初始化libc并调用main
进入真正程序主体。 ctr1.o
同时向libc 启动函数__libc_start_main()
传递了两个函数指针:_libc_csu_init
、_libc_csu_fini
,这来给你个函数负责调用_init()
、_finit()
函数。ctri.o
、ctrn.o
用于帮助实现初始化函数,这两个目标文件中包含的代码实际上是_init()
、_finit()
函数的开始和结尾部分,这两个文件和其他目标文件按照顺序链接起来以后,形成了完整的_init()
、_finit()
函数。._init
段如图所示, .init
段、.fini
段还包含了录入C++全局对象的构造/析构函数的调用函数、用户监控程序性能、调试工具等。我们也可以用__attribute((section(".init")))
将函数放入._init
段。(但是要用汇编,否则编译器产生ret
指令,使得._init
段提前返回了)GCC 平台相关目标文件
crtbeginT.o
、crtend.o
、libgcc.a
、libgcc_eh.a
。这些并不属于glibc,是GCC的一部分。
crtbeginT.o
、crtend.o
用于配合glibc实现C++的全局构造与析构
ctri.o
、ctrn.o
中的.init
段、.fini
段只是提供了一个在main()
之前/之后运行代码的机制,真正的全局构造/析构由crtbeginT.o
、crtend.o
来实现。(即上图中的中间部分)libgcc.a
包含了很多函数的运算(不同CPU对浮点数等运算方法不同);libgcc_eh.a
则包含了C++的异常处理的平台相关函数。MSVC CRT:
C++CRT:
当程序里包含了某个C++标准库头文件,MSVC编译器认为该源代码文件是一个C++源代码程序,在编译时根据选项在目标文件的.drectve
段添加相应C++标准库链接信息。链接时会加上相应的.lib
。
使用不同版本的CRT:
.lib
或DLL文件但是又没有源代码时就很难办。用一个版本的编译器编译的程序无法在别的机器上运行?
_beginthread()
、_endthread()
,Linux 的glibc 提供了可选的线程库pthread(POSIX thread)
包括了pthread_create()
、pthread_exit()
。这些都不属于标准的运行库,他们都是平台相关的。strtok()
(分解字符串函数)(局部静态变量在多线程中混乱)、malloc/new、free/delete、printf、信号等都是线程不安全的。
/MT
或/MTd
等参数指定使用多线程运行库。__thread int number
与MSVC__declspec(thread) int number
都提供了 TLS机制。.data
、.bss
段中, 当使用__declspec(thread)
定义一个线程私有变量时,编译器将其放在.tls
段中。
.tls
段中内容复制进去,使得每个线程都有独立的.tls
副本。.rdata
段中。.tls
段副本第hi、然后加上变量偏移得到TLS变量在线程中的地址。TlsAlloc()
、TlsGetValue()
、TlsSetValue()
、TlsFree()
;Linux提供了pthread_key_create()
、pthread_getspecific()
、pthread_setspecific()
、pthread_key_delete()
。CreateThread()
、ExitThread()
;_beginthread()
、_beginthreadex()
、_endthread()
、_endthreadex()
(推荐使用带ex的);AfxBeginThread()
、AfxEndThread()
;C++中,入口函数还需要在main 的前后完成全局变量的构造与析构。
glibc 全局构造与析构:
glibc启动文件介绍的.init
、.finit
段,这两个段最后拼成_init()
、_finit()
函数,分别在main()
之前/之后执行。
构造:
_init()
的调用关系:_start()
->__libc_start_main()
->__libc_csu_init()
->_init()
_init()
没有源文件,是拼出来的段编译成的。通过反汇编可以看到它调用了GCC的crtbegin.o
提供的__od_global_ctors_aux()
函数,用于构造全局变量。函数源文件位于gcc/Crtstuff.c
。
.cpp
,GCC编译器会遍历所有全局变量, 生成一个_GLOBAL__I_Hw()
函数,用于对本编译单元所有全局函数的初始化。 并在这个目标文件的.ctors
段中放置这个函数的指针。
.ctors
段的每个元素都指向一个目标文件的全局构造函数。 同时在链接时会修改.ctors
段中第一个四字节用作该列表的长度,同时尾部添加NULL(0)。
_main
函数:
.init
、.ctors
机制,于是有 Collect2 对ld链接器的封装。main()
之前执行的特殊符号,将其生成一个临时.c
文件,编译后与其他目标文件一起链接到输出文件中。_main()
函数,负责Collect2 收集起来的那些函数,_main()
函数也是GCC提供的目标文件之一。析构过程与构造过程基本类似,在生成构造函数的时候,对应的通过__cxa_atexit()
注册析构函数。
MSVC CRT的全局构造与析构:
mainCRTStartup()
,他调用了_initterm()
,其参数为两个函数指针的指针。
_initterm()
依次遍历函数指针并且调用他们,运行库中真正复杂的是软件与外部通信部分,即IO部分。
fread()
是对Windows API ReadFile()
的封装。 fread()
的参数有 buffer、count、size、stream。
缓冲机制:用于避免大量实际文件的访问。减少了系统调用的开销。
fread_s:
fread
调用了fread_s
(S ->safe)fread_s
多了一个buffersize 的参数。fread_s
首先检查参数,然后使用_lock_str
对文件加锁,然后调用_fread_nolock_s
_fread_nolock_s: 进行实际工作的函数;(利用缓冲的读取,减少了系统调用的开销)
streambuffersize
设置memcpy_s
将文件stream
里_ptr
所指向的缓冲内容复制到data
指向的位置。_read
函数真正的从文件中读取数据,读取尽可能多的证书个缓冲的数据直接进入输出位置。_filbuf
函数负责填充缓冲,_filbuf
最终也调用了_read
函数
_read
函数主要:从文本中读取数据、对文本模式打开的文件,转换回车符。_read`:
crt/src/read.c
,调用关系: fread
-> fread_s
-> _fread_nolock_s
-> _read
。ReadFlie()
函数。
ReadFlie()
函数是Windows API。文本换行:
_read
后续还要为以文本模式打开的文件 转换回车符。
0X0D 0X0A
用CR LF表示;用C语言中的字符串表示为:\r\n
\n
;macos是\r
;windows 是\r\n
;\n
,所以遇到除了linux之外的os,都需要转换。
\r\n
转换为\n
fread 回顾:
0x08
号中断作为系统调用的入口,windows使用0x2E
号中断作为系统调用入口。fread
来读取文件。fork
、read
、write
等。中断是 一个硬件或软件 发出的请求。
中断通常有两个属性:中断号、中断处理程序(ISR,intertupt service routine)
软件中断带一个参数标记中断号。使用该指令,用户可以手动触发某个中断并执行其中中断处理程序。
中断号有限,所以通常所有系统调用占用一个中断号。linux使用0x08
号,windows使用0x2E
号。
int 0x80
,会保存现场,将特权状态切换到内核态,然后查询中断向量表中0x80
号元素。
__asm__
是gcc关键字,表示下面要嵌入汇编代码。volatile
关键字表示GCC对这段代码不进行任何优化。int $0x80
为调用0x80
号中断。int
中断发生时,CPU切入内核态,还需要找到当前进程的内核栈,并在内核栈中压入用户态的SS
、ESP
、EFLAGS
、CS
、EIP
。(都是int
做的)
iret
回到用户态,并将内核栈中弹出寄存器的值,恢复用户态的栈。(都是iret
做的)SAVE_ALL
汇编将相关寄存器压入栈中,这里就包括了EAX~EDI。
sysenter
、sysexit
。
sysenter
后,系统直接跳转到由某个寄存器(eax)指定的函数执行,并自动完成特权级别转换、堆栈切换。Windows下,CRT是建立在windows API之上的,MFC也是一种C++形式封装的库。
Windows没有将系统调用公开,而是在系统调用上建立了一个API层,让程序只能调用API层的函数。
windows API概述:
kernel32.dll
、user32.dll
、gdi32.dll
。NTDLL.DLL
将内核系统调用包装了起来,是windows用户层面的最底层,所有DLL都通过 调用NTDLL.DLL
,由他进行系统调用。为何要用windows API:
API与子系统:
#ifdef WIN32
来根据编译平台确定代码部分。
stdio.h
、字符串与堆:stdlib.h
。GetCommandLine
ExitProcess
API 结束进程。
atexit()
注册的退出回调函数。malloc
和free
。
brk()
系统调用,将数据段结束地址向后调整*MB,作为堆空间。virtualAlloc
API,向系统申请*MB空间作为堆空间。
malloc
,不使用windows 的HeapAlloc
等API来分配空间。CreateFile
、ReadFile
、WriteFile
、CloseHandle
、SetFilePointer
。
GetStdHandle
的API获得。open
、read
、write
、close
、seek
等系统调用。
FILE*
类型在windows中实际是内核句柄 ,在linux中是文件描述符,并不是FILE
的指针。printf
的实现了。
首先是头文件:
编译库文件:(采用静态库的形式,动态库更加复杂)
使用:
保证了整个程序只依赖OS内核。绕过了运行库。
Kernel32.DLL
,绕过了MSVC CRT的运行库msvcr90.dll
。通常C++运行库都独立于C语言运行库,一般C++运行库依赖于C语言运行库的。
new/delete:
operator new
操作符函数的调用。
C++全局构造与析构:
atexit()
函数。
.CRT$XCA
、.CRT$XCZ
,然后定义两个函数指针分别指向他们。.crtor
段的起始和结束部分,然后定义两个函数指针分别指向他们。
atexit实现:
exit()
函数中被调用。
atexit
和exit
是c语言运行库的一部分。atexit
或类似的函数 注册的。(包括windows、linux)从而让其在程序退出时执行。
exit
时遍历执行一遍回调函数即可。入口函数修改: 在入口函数中加上do_global_crtors
,在exit函数中加上mini_ccrt_call_exit_routine
,用于全局构造和析构
生成静态库:
使用:
crtbegin.o
和crtend.o
放在用户目标文件的最开始和最后端,保证链接的正确性。