CC++程序员是否应该掌握汇编语言?

来源:互联网 发布:治疗胃溃疡的药 知乎 编辑:程序博客网 时间:2024/05/31 06:22

写的挺好的文章 http://c.chinaitlab.com/basic/863415.html


工作3年之余,发现精力会随着很多事情而降低,以前觉得很有激情很有兴趣的东西,可能会慢慢变得“无关紧要”了。不知道这是一种所谓的洒脱,还是一种懈怠。总之我会努力克服现在的状态,让自己的业余时间再充分利用起来。加上最近得了一个“准专家徽章”,为了对得起这个徽章,也为了摆脱前面的懈怠,我要坚持写下去。

  之前在网络上,一群伙计在那儿讨论关于CC++程序员是否应该对汇编语言有一定的掌握程度的问题,当然我作为一名CC++程序员也参与了讨论。对于这个问题,我有我自己的观点,可以说是亲身的感受和经历以及经验之谈。于是,抽这个中秋假期有时间,跟大家分享一下。当然,首先声明一下,本文只代表我个人的观点,您愿意接受,我感欣慰;不愿接受,就当我在这里唱独角戏。技术上的东西,每个人都有自己的一套方法,没有对错,还是我一贯的观点:我只吸收对我有用的,无用的就当看小说吧。

  首先,就本文标题而言,为什么强调是“某种汇编语言”。我想,大家都知道,汇编语言只是所有平台汇编语言的统称,这样说来,不同的平台就会有不同的汇编语言,即不同的汇编指令、不同的机制等。这里所谓的某种,即在自己经常工作的平台下的汇编语言。

  其次,对于是否应该掌握这个问题上,反对掌握的观点大致有以下几点:1、对于普通软件开发人员来讲,关注于上层实现,关注于功能和产品才是主要,汇编也用不到。

  2、很多的CC++程序员不懂汇编,也成为了某公司某项目组主程序、核心研发,因此汇编可以不学不用。

  3、绝大多数CC++程序员还是在做上层开发,绝大多数项目也是上层开发,不了解底层也能赚到大钱,而且是更多的钱。

  4、做底层,比如:逆向、破解、写病毒等,很多致力于这层的程序员,感觉整天昏天暗地,无数的重复劳动也没赚到大钱,觉得没意义。

  5、对于做C#、java、WEB等领域的程序员们,大多数不会去关注底层实现,他们照样过得很好,以此类推,汇编也是可以不掌握的。

  6、汇编语言几乎是不跨平台的,于是就算你掌握了某个平台的汇编,但你在汇编语言上,例如语法、机制等还是有局限性。

  我就暂时列举这么几种反对的观点,可能你还有其他的看法,欢迎你的回复。至于支持掌握的观点,也就是我个人的观点,我个人是站在支持一方的。所以下面就是我针对上面几种反对的观点的看法以及我个人的体会。

  从上面几种反对的观点来看,可以总体分为几个关键点:利益、收入和兴趣。我不打算将这三个关键点分别进行针对性阐述,因为它们之间都是息息相关分不开的。对于利益和收入上来讲,的确是有很多公司的高职位且收入不错的程序员并不了解汇编和底层,这样也不代表他们没有能力,而往往他们是非常有能力的人。这样一来似乎和我个人的支持类的观点有所冲突,但我认为这个冲突可以用追求二字来化解。为什么可以用追求来化解呢?因为追求可以是任何领域、任何方向、任何目的和任何标准的,因此也就是我前面所说的,归结到根本,掌握不掌握都没有对错。那我就说说我作为一个程序员的感受吧。

  我们进入这个行业,从事了编程工作(大多数程序员都是编程开发做起的)。我相信很多程序员的初衷,都是对编程开发有很大的兴趣,兴趣驱使着我们熬夜,驱使着我们研究, 驱使着我们进步。对于CC++程序员,我相信兴趣占有很大的比重。那么,我们来举几个例子:你应该看过《深入C++对象模型》这本书吧,这是一本非常细致和美妙的书,我想你应该有这样的感受。那么在此基础之上,你有过更多的思考吗?这本书里都是以实例和理论来进行讲解的,实例是以C++语言进行描述的。于是你是否有想知道在具体的编译器和平台下的这样一些疑问:1、this指针是怎么传递进成员函数的?成员函数和普通函数以及静态成员函数有何区别和联系?

  2、透过语法,成员函数和类在内存中有什么联系?对象和成员函数有何种联系?

  3、函数间的调用原理,是怎么实现的?

  4、__cdecl、__stdcall、__thiscall和__fastcall这几种函数调用方式,在本质上有什么区别?具体是怎么实现的?

  5、虚函数、多态和继承在本质上的体现,以及这些机制在底层是怎么实现的?

  除此之外,你在学习和使用CC++的时候,我想你还会在乎一些细节,例如:1、递归函数一定会导致低效?编译器针对递归函数会不会有什么样的优化?你怎么知道这些优化细节?

  2、对于这句代码:int b = a > 0 ? 100 : 200;  // int a;      编译器会有什么细节上的优化?这句代码会有比较并跳转的过程吗?

  3、对于这样的代码:view plainprint?

  #include <stdio.h> int a = 10;int main( void )

  { printf( "%d", a );return 0;}在VC下,开启全局优化,全局变量a还存在吗?你怎么通过本质的论证来证明?

  4、对于:float a = 100;  int b = a / 30;  在VC下,你会不会怀疑这两句代码背后会存在函数调用?如果有,调用了什么函数?为什么?

  5、对于__declspec( thread ) int g_nNum = 0; 你知道g_nNum++;这句代码背后的具体实现机制吗?

  6、对于调试,你会怎么根据自己记录的程序崩溃时的堆栈现场及其它信息来错误跟踪查找呢?

  7、对于开发游戏来说,你怎么知道外挂是怎么修改游戏程序的,修改了哪个地方呢?

  8、你要怎么熟悉编译器的优化细节,怎么写出适应它的代码,怎么写出比它优化得更好的代码?

  还有很多这样上层开发的例子,这里就不一一列举了。从上面列举的这些疑问,我想作为一名CC++程序员,热爱编程开发的程序员来讲,你都想知道其中的原委。作为上层开发者,是应该关注上层的功能开发和产品方面的东西。但是我个人觉得,本着技术,本着这份热爱,本着自身的技术发展,了解更底层一些,有助于上层开发的通透性,以及全局的掌控力度。从我个人的感受来讲,当把握了关键细节以及全局设计之后,任何地方出了问题,都能很及时的反应并予以追查和处理。在当前的编译器技术上,已经非常强大了,很多细节可以放心的交给编译器来优化和处理。但是我想,编译器不是万能的,人才是最智能的。掌握不是必然,但掌握了会更好。

  对于初学者乃至工作了一定时间的程序员,对于CC++,很多处于CC++语法的层面,在语法上的条条款款使用得得心应手,问其本质,可能就缺乏一二了。我个人的经历来看,在掌握语法之后,在向下关注一下语法背后的具体实现,会通透很多。你会在内存上、数据上和程序的各种底层运行机制上会有深刻的认识。这也就是为什么去海边玩儿,还要潜水去看看海底世界。错过了海底,你会失去很多精彩,而这些精彩我想也是作为程序员应有的追求之一。

  对于java、C#以及WEB类领域的程序员,我想汇编可能相对遥远一些。在这方面的关注也会相对少一些,但结合前面的观点,作为程序员这个角色,都是让自己的程序在机器上面跑起来,那么我想这之间的诸多底层的疑问可以作为程序员的一种兴趣来研究。目的也是为了让自己更通透,更熟悉自己的平台。我不知道怎么表达通透二字,就我个人的感受就是,能够从现象联系到本质实现,并且能够从本质实现勾勒出一幅很清晰生动的图像在脑子里,一切都一目了然尽收眼底。有点居高临下,望长城内外,惟余莽莽的那种宽广的感触。

  对于本身就处于底层开发的程序员来说,无可厚非,掌握汇编就是必须的了。但是澄清一点,本文的观点更多的是从兴趣和通透性上出发,对于底层开发者可能会觉得底层有一定的枯燥,特别是整天破解、逆向等工作,非常多的体力活,从我几年的业余破解和逆向经验来看的确是这样的。但是我觉得,破解和逆向只是领域之一,我之所以破解和逆向,很多时候是处于兴趣和为了对上层进行更本质和合理的解释。所以,上层和底层结合,才是我的根本目的,也是本文想推崇的一种思路。

  综上所述,我的观点是CC++程序员乃至程序员,不管是作为兴趣还是工作,掌握或者了解一下汇编都是有一定必要的,但不是强制性的,也正所谓需求和追求不尽相同罢了。因此,不要问别人到底是否应该关注一下底层,掌握一下某种汇编语言,答案很明显。


介绍函数的一篇文章C++函数调用过程深入分析  

http://blog.csdn.net/dongtingzhizi/article/details/6680050

C++函数调用过程深入分析

作者:靠谱哥

微博:洞庭之子-Bing

0. 引言

  函数调用的过程实际上也就是一个中断的过程,那么C++中到底是怎样实现一个函数的调用的呢?参数入栈、函数跳转、保护现场、回复现场等又是怎样实现的呢?本文将对函数调用的过程进行深入的分析和详细解释,并在VC 6.0环境下进行演示。分析不到位或者存在错误的地方请批评指正,请与作者联系。

  首先对三个常用的寄存器做一下说明,EIP是指令指针,即指向下一条即将执行的指令的地址;EBP为基址指针,常用来指向栈底;ESP为栈指针,常用来指向栈顶。

  看下面这个简单的程序并在VC 6.0中查看并分析汇编代码。

图1

1. 函数调用

  g_func函数调用的汇编代码如图2:

图2

   首先是三条push指令,分别将三个参数压入栈中,可以发现参数的压栈顺序是从右向左的。这时我们可以查看栈中的数据验证一下。如图3所示,从右边的实时寄存器表中我们可以看到ESP(栈顶指针)值为0x0012FEF0,然后从中间的内存表中找到内存地址0x0012FEF0处,我们可以看到内存中依次存储了0x00000001(即参数1),0x00000002(即参数2),0x00000003(即参数3),即此时栈顶存储的是三个参数值,说明压栈成功。

图3

 

  然后可以看到call指令跳转到地址0x00401005,那么该地址处是什么呢?我们继续跟踪一下,在图4中我们看到这里又是一条跳转指令,跳转到0x00401030。我们再看一下地址0x00401030处,在图5中可以看到这才是真正的g_func函数,0x00401030是该函数的起始地址,这样就实现了到g_func函数的跳转。

图4

 

图5

 

2. 保存现场          

  此时我们再来查看一下栈中的数据,如图6所示,此时的ESP(栈顶)值为0x0012FEEC,在内存表中我们可以看到栈顶存放的是0x00401093,下面还是前面压栈的参数1,2,3,也就是执行了call指令后,系统默认的往栈中压入了一个数据(0x00401093),那么它究竟是什么呢?我们再看到图3,call指令后面一条指令的地址就是0x00401093,实际上就是函数调用结束后需要继续执行的指令地址,函数返回后会跳转到该地址。这也就是我们常说的函数中断前的“保护现场”。这一过程是编译器隐含完成的,实际上是将EIP(指令指针)压栈,即隐含执行了一条push eip指令,在中断函数返回时再从栈中弹出该值到EIP,程序继续往下执行。

图6

  继续往下看,进入g_func函数后的第一条指令是push ebp,即将ebp入栈。因为每一个函数都有自己的栈区域,所以栈基址也是不一样的。现在进入了一个中断函数,函数执行过程中也需要ebp寄存器,而在进入函数之前的main函数的ebp值怎么办呢?为了不被覆盖,将它压入栈中保存。

下一条mov ebp, esp 将此时的栈顶地址作为该函数的栈基址,确定g_func函数的栈区域(ebp为栈底,esp为栈顶)。

  再往下的指令是sub esp, 48h,指令的字面意思是将栈顶指针往上移动48h Byte。那为什么要移动呢?这中间的内存区域用来做什么呢?这个区域为间隔空间,将两个函数的栈区域隔开一段距离,如图7所示。而该间隔区域的大小固定为40h,即64Byte,然后还要预留出存储局部变量的内存区域。g_func函数有两个局部变量x和y,所以esp需移动的长度为40h+8=48h。

图7

  接下来的几行指令(如下)是将刚才留出的48h的内存区域赋值为0CCCCCCCCh。

00401039   lea        edi,[ebp-48h]

0040103C   mov      ecx,12h

00401041   mov            eax,0CCCCCCCCh

00401046   rep stos    dword ptr [edi] 。

  接下来三条压栈指令,分别将EBX,ESI,EDI压入栈中,这也是属于“保护现场”的一部分,这些是属于main函数执行的一些数据。EBX,ESI,EDI分别为基址寄存器,源变址寄存器,目的变址寄存器。

 

3. 执行子函数

  继续往下看,接下来是局部变量的x和y的赋值,看汇编指令中是怎样去计算x和y的内存地址的呢?如图8所示,是基于ebp去计算的,分别是[ebp-4]和[ebp-8]。我们查看内存表可以看到相应的内存区域已经存入了0x11111111和0x22222222。

图8

  此时我们对整个内存区域中存储的内容应该非常清晰了(如图9所示)。

图9

 

4. 恢复现场

  这时子函数部分的代码已经执行完,继续往下看,编译器将会做一些事后处理的工作(如图10所示)。首先是三条出栈指令,分别从栈顶读取EDI,ESI和EBX的值。从图9的内存数据分布我们可以得知此时栈顶的数据确实是EDI,ESI和EBX,这样就恢复了调用前的EDI,ESI和EBX值,这是“恢复现场”的一部分。

图10

  第四条指令是mov esp, ebp 即将ebp的值赋给esp。那这是什么意思呢?看看图9的内存数据分布,我们就能很明白了,这条语句是让ESP指向EBP所指的内存单元,也就是让ESP跳过了一段区域,很明显跳过的恰好是间隔区域和局部数据区域,因为函数已经退出了,这两个区域都已经没有用处了。实际上这条语句是进入函数时创建间隔区域的语句 sub esp, 48h的相反操作。

  再往下是pop ebp,我们从图9的内存数据分布可以看出此时栈顶确实是存储的前EBP值,这样就恢复了调用前的EBP值,这也是“恢复现场”的一部分。该指令执行完后,内存数据分布如图11所示。

图11

  再往下是一条ret指令,即返回指令,他会怎么处理呢?注意在执行ret指令前的ESP值和EIP值(如图12所示),ESP指向栈顶的0x00401093,EIP的值是0x0040105C(即ret指令的地址)。

图12

  执行ret指令后我们来查看ESP和EIP值(如图13所示),此时ESP为0012FEF0,即往下移动了4Byte。显然此处编译器隐含的执行了一条pop指令。再来看一下EIP的值,变为了0x00401093,这个值怎么这么熟悉呢!它实际上就是栈顶的4Byte数据,所以这里隐含执行的指令应该是pop eip。而这个值就是前面讲到过的,在调用call指令前压栈的call的下一条指令的地址。从图13中可以看出,正是因为EIP的值变成了0x00401093,所以程序跳转到了call指令后面的一条指令,又回到了中断前的地方,这就是所谓的恢复断点。

图13

  还没有完全结束,此时还有最后一条指令add esp, 0Ch。这个就很简单了,从图13中可以看出现在栈顶的数据是1,2,3,也就是函数调用前压入的三个实参。这是函数已经执行完了,显然这三个参数没有用处了。所以add esp, 0Ch就是让栈顶指针往下移动12Byte的位置。为什么是12Byte呢,很简单,因为入栈的是3个int数据。这样由于函数调用在栈中添加的所有数据都已清除,栈顶指针(ESP)真正回到了函数调用前的位置,所有寄存器的值也恢复到了函数调用之前。

 

结束!



0 0
原创粉丝点击