2023年8月1日发(作者:)
CC++的编译与运⾏/cwbo-win/p/
C/C++编译前,⾸先要对源代码执⾏预处理。预处理器(preprocessor)是⼀个简单的程序,它⽤程序员(利⽤预处理器指令)定义好的模式代替源代码中的模式(删除注释、包含其他⽂件以及执⾏宏),预处理后⽣成中间⽂件.i(⽂本)。 接下来对于.i⽂件进⾏语法分析。编译器把源代码分解成⼩的单元并把它们按树形结构组织起来。表达式中“A + B”中的“A”、“+”和“B”就是语法分析树的叶⼦节点。语法分析树建⽴后有时会根据⽤户定义,使⽤全局优化器(global optimizer)来⽣成更短、更快的代码。
全局优化器主要是进⾏以下优化:局部和全局公共⼦表达式消除在此优化中,计算⼀次公共⼦表达式的值。在下⾯的⽰例中,如果 b 和 c 的值在三个表达式之间没有更改,则编译器可以将 b+c 的计算分配给⼀个临时变量,并⽤此变量替代 b + c:
a = b + c;d = b + c;e = b + c;对于局部公共⼦表达式优化,编译器检查公共⼦表达式的⼀⼩部分代码。对于全局公共⼦表达式优化,编译器搜索全部函数中的公共⼦表达式。⾃动寄存器分配此优化允许编译器将常⽤变量和⼦表达式存储在寄存器中;忽略 register 关键字。循环优化此优化将不变量⼦表达式从循环体中移除。最佳循环只包含其值在每次循环执⾏过程中都要更改的表达式。在下⾯的⽰例中,表达式 x+y 在循环体中不更改:
i = -100;while( i < 0 ) { i += x + y;}优化之后,计算⼀次 x + y ⽽不是每次执⾏循环时都计算:
i = -100;t = x + y;while( i < 0 )
{ i += t;}当编译器不能假定任何别名时(通过 __restrict、noalias 或 restrict 设置),循环优化更有效。提⽰:在VS的编译中使⽤带 g 选项的 optimize 杂注,可以逐个函数地启⽤或禁⽤全局优化。 优代完成之后,还需要使⽤代码⽣成器(code generator)遍历语法分析树,把树的每个节点转化成汇编语⾔,这个期间保存为中间⽂件.s(汇编语⾔ ⽂本)。之后根据⽤户定义可以使⽤窥孔优化器(peephole optimizer)从相邻⼀段代码中查找冗余汇编语句。 窥孔优化,顾名思义,是⼀种很局部的优化 ⽅式,编译器仅仅在⼀个基本块或者多个基本块中,针对已经⽣成的代码,结合CPU⾃⼰指令的特点,通过⼀些认为可能带来性能提升的转换规则,或者通过整体 的分 析,通过指令转换,提升代码性能。别看这些代码转换很局部,很⼩,但可能会带来很⼤的性能提升。这个窥孔,你可以认为是⼀个滑动窗⼝,编译器在实施窥孔优化时,就仅仅分析这个窗⼝内的指令。每次转换之后, 可能还会暴露相邻窗⼝之间的某些优化机会,所以可以多次调⽤窥孔优化,尽可能提升性能。窥孔优化可以在四个⽅⾯寻找优化机会:冗余指令删除,包括冗余的load和store指令以及死代码(不会执⾏的代码);控制流优 化;强度削弱;利⽤特有指令。删除冗余load和store,⽐如这段汇编代码: sh $0,6($sp)sh $0, 4($sp)sh $0, 2($sp)sh $0, 0($sp)ldc1 $f1, 0($sp)完全可以使⽤指令xor $f1,$f1,$f1这样可以省掉5次访存操作,性能的提升⾮常明显。上⾯sh是store 16bit到某个地址,ldc1是load 64bit到某个寄存器。xor是异或指令。但是这种指令序列的转换和合成有个前提,必须保证这些指令按照顺序执⾏,即这些指令之间,不能有其他标号,即⼊⼝。也就是说这些指令必须在⼀个基本块中。当然,你也可以在编译器较前⾯阶段的优化中,针对该操作,做变换。这样到了窥孔优化时,就不再会有这样的代码了。删除死代码:有 些代码可能⽤于不会被执⾏到,这样在窥孔优化阶段,如果发现这样的代码,就可以直接删除。典型的⽅式是优化双跳转,即第⼀条跳转指令的⽬的地址还是⼀条跳 转指令时,可以删除后⼀条跳转指令,并修改第⼀条跳转指令的⽬标地址。另外,对于不可能进⼊的分⽀也可以使⽤这种⽅式删除。控制流优化:中 间代码⽣成阶段,很可能经常产⽣⼀些跳转到跳转指令,跳转到分⽀跳转、分⽀跳转到跳转之类的指令,都可以在窥孔优化中想办法解决掉,当然你也可以在中间代 码中优化它们。⼀些分⽀被删除后,可能还存在⼀些不会被到达的标号(label),也可以顺便删除之,这样就会提升基本块的⼤⼩,增加优化机会。强度削弱:即 利⽤代价较⼩的指令或操作替代代价较⼤的指令或操作,从⽽提升性能。⽐如x=x+0, x=x*1之类的操作就能直接避免,x=x*2,x=x/2之类的操作可以使⽤左移或右移实现。x^2之类的指数运算可以削弱为x*x的乘法运算,浮点数 除以常数的运算可以转换为浮点数乘以常数的倒数。这些都是优化⽅式。充分利⽤特有指令:CPU都会提供⼀些特殊指令完成特殊操作,⽐如DSP芯⽚中可能有复杂的数字信号处理指令,龙芯中有乘加指令以及⼀些向量扩展指令。还有⼀些CPU可能提供⾃增、⾃减、取绝对值指令。这些都能在窥孔优化中⽣成。提升程序的运⾏性能。 接下来使⽤汇编器将汇编源⽂件翻译成对应的机器指令,⽽且还写⼊⼀些东西与机器指令打包成可重新定位⽬标程序格式的⽂件,⽣成中间⽂件.o(⽬标⽂件 ⼆进制)。最后使⽤连接器(linker)把⼀组⽬标模块连接成为⼀个可执⾏程序(最终⽂件), 简单的讲,把⽬标的库⽂件和所需要的引⽤的静、动态链接库进⾏链接,即需要把其他静态库合成到可执⾏⽂件中,转换相应的符号引⽤为地址,然后确保所引⽤的 其他动态链接库的符号存在。此外,链接器还要完成程序中各⽬标⽂件的地址空间的组织,这可能设计重定位⼯作。⼤多数现代操作系统都提供静态链接和动态链接 两种形式。
注:静态类型检查是在建⽴语法分析树时完成的。
windows程序的启动过程命令解释器(shell)调⽤了CreateProcess系统函数,创建⼀个“进程内核对象”。进程内核对象可以看作⼀个操作系统⽤来管理进程 的内核对象,它也是系统⽤来存放关于进程统计信息的地⽅(⼀个⼩的数据结构),其实它的真正创建者是⼀个叫NtCreateProcess的windows2000系统服务函数(也叫执⾏体服务函数),他创建了进程内核对象供⽤户扩展。进程内核对象的初始使⽤计数为1。然后系统为该进程创建 4GB(=2^32)的虚拟地址空间(所谓虚拟就不是真的创建4GB的物理内存空间,这些空间不是真在物理内存上)。⽤于加载可执⾏⽂件和 任何必要的dll⽂件的数据和代码。4G的虚拟内存中,⽤户进程可以占有2GB的私有地址空间;操作系统占有剩余的2GB空间。在32位x86系统中:从0x00000000到0x7fffffff的空间中存放着 应⽤程序代码,全局变量,每个线程堆栈,dll代码。从0x80000000到0xc0000000的空间中存放着 内核和执⾏体,HAL(硬件抽象层),引导驱动程序。从0xc0000000到0xc0800000的空间中存放着 进程页表和超空间。从0xc0800000到0xffffffff的空间中存放着 系统⾼速缓存,分页缓冲池,⾮分页缓冲池。CreateProcess打开应⽤程序⽂件,它先扫描该⽂件的⽂件头,该⽂件头⾥含有⽂件能运⾏在哪个环境之下,如果是win32环境,系统就 直接加载⽂件的代码和数据并输⼊(import)该⽂件执⾏所需的动态链接库。如果不是win32环境⽐如时os/2的.exe则先加载相应的环境⼦系 统,载由该环境加载该⽂件的代码和数据以及该⽂件执⾏所需的动态链接库。具体加载动态链接库过程如下加载器(loader)读⼊可执⾏程序的导⼊符号表,根据这些符号表可以查找出该可执⾏程序的所有依赖的动态链接库。加载器针对该程序的每⼀个动态链接库调⽤LoadLibrary(1)查找对应的动态库⽂件,加载器为该动态链接库确定⼀个合适的基地址。如果该基地址和动态链接库希望记载的基地址不同,加载器还要为该库做rebase,然后把整个动态链接库映射到进程的虚拟内存空间中。(2)加载器读取该动态链接库的导⼊符号表和导出符号表,⽐较应⽤程序要求的导⼊符号是否匹配该库的导出符号。(3)针对该库的导⼊符号表,查找对应的依赖的动态链接库,如有跳转,则跳到第三步(4)调⽤该动态链接库的初始化函数进程加载代码和数据完毕后,就开始创建线程来执⾏进程空间内的代码。进程是静态的,它只是线程的容器。⼀个进程⾄少因该有⼀个线程(main thread),其它线程都是主线程通过调⽤CreateThread函数创建的。线程也是核⼼对象,他的实际创建者是⼀个叫NtCreateThread的windowsNT系统服务函数。⼀个线程其实只是⼀个线程核⼼对象和两个堆栈(⼀个核⼼堆栈,⽤于线程运⾏在核 ⼼态;⼀个⽤户堆栈,⽤于线程运⾏在⽤户态),线程与进程类似,也拥有线程核⼼对象计数和线程句柄,这⾥不详述。线程⽤于描述进程中的运⾏路径。每当进程 被初始化时,系统就要创建⼀个主线程。该线程与c/c++运⾏时库的启动代码⼀道开始运⾏,启动代码则调⽤进⼊点函数(就是我们的main函数,它也是主 线程的进⼊点函数),并且继续运⾏直到进⼊点函数返回并且c/c++运⾏时库的启动代码调⽤ExitProcess为⽌。每个线程函数必须有⼀个返回值, 它将作为线程的退出代码。对于主线程来说,这个返回值将传给c/c++运⾏时库的启动函数。c/c++运⾏时库的启动函数其实是⼀个程序的真正调⽤的第⼀个函数,它是在程序链接时由链接程序选择相应的启动函数并加到程序的开始处。c/c++运⾏时库有四个版本的启动函数,他们分别对应不同类型的应⽤程序。⽐如:需要ANSI字符和字符串的GUI应⽤程序的启动函数是WinMainCRTStartup,其对应的进⼊点函数是WinMain需要Unicode字符和字符串的GUI应⽤程序的启动函数是wWinMainCRTStartup,其对应的进⼊点函数是wWinMain需要ANSI字符和字符串的CUI应⽤程序(如控制台console程序)的应⽤程序的启动函数是mainCRTStartup,对应的⼊⼝点函数为main需要Unicode字符和字符串的CUI应⽤程序(如控制台console程序)的应⽤程序的启动函数为wmainCRTStartup,对应的⼊⼝点函数为wmainc/c++运⾏时库的启动函数的功能如下(以wWinMainCRTStartup为例): *检索指向新进程的完整命令⾏指针; *检索指向新进程的环境变量的指针; *对c/c++运⾏时的全局变量进⾏初始化; *对c运⾏期的内存单元分配函数(⽐如malloc,calloc)和其他低层I/O例程使⽤的内存栈进⾏初始化。 *为C++的全局和静态类调⽤构造函数。当这些初始化⼯作完成后,该启动函数就调⽤wWinMain函数进⼊应⽤程序的执⾏。当wWinMain函数执⾏完毕返回时,wWinMainCRTStartup启动函数就调⽤c运⾏期的exit()函数,将返回值(nMainRetVal)传递给它。之后exit()便开始收尾⼯作: *调⽤由_onexit()函数调⽤和注册的任何函数。 *为C++的全局和静态类调⽤析构函数; *调⽤操作系统的ExitProcess函数,将nMainRetVal传递给它,这使得操作系统能够撤销进程并设置它的exit()代码。以上就是windows 环境下可执⾏程序的启动过程Linux程序启动过程输⼊命令,回车exec系统调⽤接管,为应⽤程序的运⾏准备⼀些环境便利爱那个等,并且为运⾏的命令找到相应的解释器。通常应⽤程序解释器就是ld,ld接管控制权后先需要读取这个可执⾏程序的⽂件的⼀部分,包括⽂件头及共享对象(so⽂件)针对每⼀个依赖的库,ld需要⾸先读⼊这个so的⼀部分⽂件头和相关信息,然后递归查找该共享对象所依赖的其它共享对象,直到最底层。ld会把所有依赖的so映射到该程序进程空间的虚拟内存中(注意是 映射不是读⼊),由于,每⼀个共享对象在该进程的虚拟内存空间中占据不同的连续区域,他们的“基地址各不相同”,从⽽其内部的⼀些⽤绝对地址表⽰的符号需要做出相应的修改初始化应⽤程序的全局变量,对于全局对象⼦哦的那个调⽤构造函数从⼊⼝函数开始执⾏
发布者:admin,转转请注明出处:http://www.yc00.com/web/1690873878a452166.html
评论列表(0条)