【面经笔记】装载,CRT

来源:互联网 发布:bl漫画软件 编辑:程序博客网 时间:2024/06/05 20:37

双击可执行文件发生了什么

  • 创建一个独立的虚拟地址空间

创建虚拟地址空间不是创建空间,而是创建映射函数所需要的相应的数据结构。

  • 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系

上一步是建立虚拟空间到物理内存的映射关系,这一步是建立虚拟空间与可执行文件的映射关系。

  • 将cpu的指令寄存器设置为可执行文件的入口地址,启动运行。

装载PE可执行文件过程:

RVA(Relative Virtual Address): 表示一个相对虚拟地址,它是相对于PE文件的装载基地址的偏移地址。

一个PE文件在装载时会有一个装载目标地址,即基地址。PE文件被设计成可以装载到任何地址,所以基地址不是固定的,每次装载都可能会变化。无论基地址怎么变化,RVA都保持一致。如果装载地址不是目标地址,则进行Rebasing。

1、先读取文件的第一页,这个页中,包含了DOS头,PR文件头和段表
2、检查进程地址空间中,目标地址(基地址)是否可用,如果不可用,则另外选一个装载地址。这对于可执行文件不存在:它是第一个装载的模块,基地址不会被占用。主要是针对DLL文件的装载而言的。
3、使用段表中提供的信息,将PE文件中所有段一 一映射到地址空间中的相应位置。
4、如果装载的地址不是目标地址,进行rebasing(针对dll而言)
5、装载PE文件所需要的DLL文件
6、对PE文件中的所有导入符号进行解析
7、根据PE头中指定参数,建立初始化栈和堆
8、建立主线程并启动进程

运行库

9、操作系统创建进程后,把控制器交到程序的入口,这个入口是运行库中的某个入口函数
10、入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造
11、入口函数完成初始化后,调用main函数,正式开始执行程序的主体部分。
12、main函数执行完后,返回到入口函数,入口函数进行清理工作,包括全局变量析构,堆销毁、关闭I/O等,然后进行系统调用结束进程。


MSVC CRT

操作系统创建进程(装载了),shell调用名为加载器的函数,它拷贝可执行文件中的代码和数据到存储器。然后将控制权交给程序的入口,即运行时库(CRT,C run-time library)的入口函数:

(1)mainCRTStartup(或 wmainCRTStartup) //使用 /SUBSYSTEM:CONSOLE 的应用程序

(2)WinMainCRTStartup(或 wWinMainCRTStartup)//使用 /SUBSYSTEM:WINDOWS 的应用程序

(3)_DllMainCRTStartup //调用 DllMain(如果存在),DllMain 必须用 __stdcall 来定义

在默认情况下,如果你的程序中使用的是main()或_main()函数,这连接器会将你的使用(1)中的函数连接到你的exe中;如果你的函数是以WinWain()函数开始的则连接器使用(2)中的函数连接进exe中;如果我们写的是DLL程序则连接进DLL的是(3)中的函数。

mainCRTStartup总体流程是:

(1)、初始化和OS版本有关的全局变量
(2)、初始化堆:_heap_init():调用heapcreate() API创建系统堆。
(3)、初始化I/O:建立打开文件表:写入父进程继承的打开的文件句柄、操作系统标准输入输出。
(4)、获取命令行参数和环境变量
(5)、初始化C库的一些数据
(6)、调用main并记录返回值
(7)、检查错误并返回main返回值


C/C++运行库基本内容(MSVC)

任何一个C程序,背后都有一套庞大的代码进行支撑,以使得该程序正常运行。这套代码至少包含入口函数,以及其所依赖的函数所构成的函数集合;各种标准库函数的实现。

这样一个代码集合称之为运行时库,C运行库:CRT

一个C语言运行库大致包含如下功能:

  • 启动与退出:包括入口函数及其依赖的函数
  • 标准函数:标准库的函数实现:标准输入输出,文件操作,字符操作,数学函数,资源管理,格式转换等
  • I/O:I/O功能的封装和实现
  • 堆:堆的封装和实现
  • 语言实现:语言中一些特殊功能实现
  • 调试:实现调试功能的代码

最初CRT设计时没有考虑到多线程环境,CRT针对多线程改进:

  • 使用线程局部存储(TLS)
    一般全局变量和静态变量会被放到.data或.bss段中,当我们使用_declspec(thread)定义一个线程私有变量时,编译器会把这个变量放到PE文件的“.tls”段中。当系统启动一个新的线程时,它会从进程的堆中分配一块足够大小的空间,然后把“.tls”段中的内容复制到这块空间中,于是每个线程都有自己独立的“.tls”副本。对于用_declspec(thread)定义的同一个变量,它们在不同的线程中的地址是不一样的。

线程如何访问这些变量呢?

每个线程都有线程环境块(TEB,Thread Environment Block)。这个结构保存了线程堆栈地址,线程ID等,其中有一个域是TLS数组地址。数组默认64个元素。

  • 多线程版本运行库中,线程不安全的函数内部会自动地加锁
  • 改进函数调用方式

Q&A:

CreateThread()和_beginthread()有什么不同?

_beginthread是对CreateThread的包装,它在调用createthread之前申请了一个叫tiddata的结构。这个结构被保存到TLS数组,然后调用用户的线程入口真正开始线程。

如果使用CreateThread创建线程,线程使用_tidata时,若发现没有_tidata结构时会马上申请这个结构。
但是ExitThread()不会释放_tidata结构内存,而_endthread()则会释放。

但是,很多时候即使使用CreateThread()/ExitThread(),也不会造成内存泄漏,为什么?

原因在于DLLMain函数,每次进程/线程开始退出的时候,每个DLL的DllMain都会被调用一次,于是动态链接版的CRT 就有机会在DllMain中释放线程的_tiddata。

可是只有当CRT是动态链接版的时候才起作用,静态链接CRT没有DllMain,这就造成了使用CreateThread()会造成内存泄漏。


联合和结构的区别

手写单例模式

指针和引用区别

全局变量的声明和类的构造函数

CRT运行时库

用过哪些windows的API

const关键字、 define关键字区别

静态链接、动态链接

多进程实现方式,具体细节

看过哪些书

原创粉丝点击