VC与Delphi都是windows下的开发工具,它们的编译系统都是与CPU及操作系统相关系的。VC使用的是C++语言与C++标准库,而Delphi使用的是object pascal语言,与delphi运行时库, 它们组成windows程序的策略是不一样的。
一、运行库实现策略存在区别:
在用VC开发程序时,如果是win32应该程序,则用户级入口函数为winmain,而如果是控制台程序,也就是防dos程序(在windwos下是需要在cmd这个平台中执行的),则用户级入口函数为main。而用delphi开发程序时,不管是win32程序还是控制台程序都是program关键字开头的入口函数。在这里需要强调的一点是,对于vc程序,真正的入口函数不是winmain或者main函数,而是运行时库中的winmainctrstartup或者mainctrstartup函数。它是运行时库调用winmain或者main函数来运行程序的。而对于delphi程序,真正的入口函数就是program关键字开头的函数,它来调用运行时中的initexe函数来运行程序。所以它们区别就在于前者运行时库是调用则,而后者运行时库就是被调用者。(对于delphi的运行时库,borland公司是将源代码公布出来的,而不像microsoft公司没有公开运行时库的源代码,所以想学习运行库的运行机制delphi是最佳之选)
二、对于异常处理上存在差别:
由于delph程序的运行时库时嵌套在program中,所以运行时库没有对异常的处理,当在program中发生未处理的异常时,就直接返回到了操作系统中,操作系统弹出一个消息框,在其中还会调用delphi运行时库的函数,弹出了异常框。(所以对于program中发生的未处理的异常,必定先到操作系统,再到运行时库, 这个估计delphi编译系统设置了操作系统SEH结构中的相应地址。)而对于VC程序的运行时库是包含了winmain函数的,所以当在winmain函数中发生异常时,就会返回到运行时库来处理异常,一般性运行时库会调用terminate函数(这个函数默认会调用abort函数,不过开发人员通过set_terminate函数可以设置自己的)。这里有一点需要强调的时对于除0异常以及访问内存异常这两个异常运行时库是不会处理的,所以这两个异常会返回到操作系统,操作系统弹出一个消息框。(所以对于winmain程序中发生的未处理异常,必定先到运行时库,再到操作系统中。) 关于除0以及内存访问非法地址这种异常,不是因为遇到了像raise,throw语句,而是由cpu触发的异常(估计这时cpu会软中断到操作系统的内核中,操作系统根据SEH结构机制来进入到某程序的异常处理例程中,如果没有找到,就返回到操作系统本身中)
三、对函数的调用实现机制上也是有所区别的:
delphi中默认的函数调用约定为register,即前三个参数可以放入相应的寄存器中,其他的也是推入到堆栈中,这样效率更高,参数传入顺序也是从右到左的,例程自己负责对参数的清栈(但帮助手册上说是从左到右的,我通过汇编查看发现是从右到左的) 而对于vc中默认的函数调用约定为__cdecl,它所有参数是从右到左推入到堆栈中的,调用者负责对参数的清栈。对于函数中局部变量的堆栈分配策越也是不同的,在delphi程序中register指示函数中,比如如下:
function getv1(a,b: Integer): Integer;
var
x,y: Integer;
begin
x := 12;
y := 35;
Result := getv(a+ x, y+ b);
end;
对应的汇编如下(我就举例说几个,其他大家自己看看就行,这几条汇编比较简单):
1、push ebp 是先将调用函数的栈底推入堆栈中,保证被调函数返回时,调用函数能正常执行。
2、move ebp,esp 是将当前栈顶的位置保存到esp中,作为当前函数的栈底。
3、add esp, -$14 这句汇编是关键,它就是给此函数分配了堆栈空间,使得栈顶向低位置移动20个字节。这20个字节刚好为a,b,x,y,result变量的总大小(5*4)。a: [ebp- $04]
b: [ebp- $08] result: [ebp- $0c] x: [ebp -$10] y: [ebp -$14] 也就是a,b参数是在自己函数的堆栈空间中的。
4、move [ebp-$08],edx 是将刚才调用函数的实参传给形参变量b,register就是通过寄存器来传参值的。
5、move [ebp -$0c],eax 就是将它本身调用的getv函数的返回值放入result变量中。注意:在各个编译系统中都是基本上协定eax这个寄存器来保存返回值的。
6、move esp, ebp 这句汇编也是很关键,它就是负责清栈,就是将刚才分配的栈空间进行回收。因为esp一开始就保存了栈底地址。(注意:其中也将参数占用的栈空间也清除了,这就是register例程的特性。)
7、pop ebp 就是将刚才推入堆栈的调用函数的本身ebp值弹出到ebp寄存器中。这样在函数返回时,调用函数能正常往下执行。
8、ret 函数返回,相当于执行了pop 跟jmp指令。
以下vc程序中__cdecl指示函数的处理情况,例子如下:
int getv1(int a, int b)
{
int x = 12, y = 35;
return getv(a + x, y + b);
}
对应的汇编如下:
1、对于 push ebp 与move ebp esp 这两条汇编语句与delphi中的意义是一样的,这里不在讲了。
2、sub esp 48h 这条汇编语句我是不变明白(请高手指点),vc编译系统为什么要分配72个字节的堆栈空间了,这不是浪费嘛,而delphi编译系统中就是刚刚好的。这里的堆栈分配策略与delphi所不同的,而且里边是不包含参数栈空间的。
3、push ebx push esi push edi 先将这三个寄存器的值保存起来,因为此函数操作中改变此寄存器的值。
4、lea edi, [ebp-48h] mov ecx,12h mov eax,0CCCCCCCCh rep stos dword ptr [edi] 这几条汇编就是将[ebp-48h] 这个堆栈空间初始化,0CCCCCCCCh的值。所以你会发现如果对C函数中的局部变量没有初始化的值时,都是这个0CCCCCCCCh值。
5、mov dword ptr [ebp-4],0Ch 这就将12常量值赋值到x 变量中。 x:[ebp-4] y:[ebp-8]
6、mov eax,dword ptr [ebp-8] add eax,dword ptr [ebp+0Ch] push eax 这几条汇编就是实现y+b表达式的计算,并推入到堆栈中,供getv函数使用。ebp+0Ch这个地址是关键,它代表就是调用函数传入进来b参数的值,为什么要+0c呢,其实很简单,上层函数的堆栈地址肯定比内层函数的地址要高。(因为堆栈的分配策略是从高地址到低地址的)。
7、call @ILT+20(getv) (00401019) add esp,8 这两条汇编就是调用getv函数,并回收刚才push eax,push ecx两条汇编语句占用8字节的堆栈空间(也就是存放传给getv函数两个参数的值)。在这里我们就看出了__cdecl函数的调用参数的栈空间就是调用者来清除的。
8、add esp,48h cmp ebp,esp call _chkesp (004017be) 这三条汇编语句主要检查刚才分配的堆栈空间有没有发生内存异常,具体为什么要这样检查下本人不清楚。(希望高手指点)
9、mov esp,ebp pop ebp ret 这三条汇编语句就是将刚才分配的局部变量的栈空间进行回收,并弹出上层函数的ebp值到ebp寄存器中,然后就返回了。
四、两者编译系统对于dll处理上也有区别:
在delphi编译系统中对dll的处理机制是通过external关键字来实现了,它没有像VC编译系统中存在导入库文件的概念。不过在对bpl的实现上其实也有一个类似于vc到导入库文件的dcp文件。delphi编译系统当遇到external关键字时,会生成一个代理函数,这个代理函数会跳转到dll的导出函数中去的。而vc编译系统会提供一个导入库文件,其实这个导入库文件中就是存在每个导出函数的代理函数,编译系统链接的都是进行代理函数。在这里我讲一下关于dll的实现原理,其实dll是windows操作系统下的一种技术(关于动态链接与静态链接的区别我会在下一篇文章中提及。) 在windows应用程序其实就是一种文件格式,就是PE格式,对于操作系统来说它就是一个文件,操作系统会来处理这个文件的。像delphi、vc编译系统在链接程序时会链接成PE格式的应用程序,其中调用的dll信息也会放入到PE文件的相应格式中。而对于代理函数跳转到dll的原始函数中,其实是有一张dll函数入口表的,这也是windows约定好的。编译系统只要将这张表的入口逻辑地址写入的PE文件的相应格式中进行了。当在启动程序时,windows操作系统会根据PE格式中的dll的信息加载相应的dll,并将导出函数的地址得到后放入到那张表中,这样代理函数根据这张表就可以跳转到原始函数中了。
————————————————
版权声明:本文为CSDN博主「sjjwan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sjjwan/article/details/8467874