在香港买iPhone7pius256苹果7pius128g多少钱钱

7849人阅读
技术总结(1)
  调试是程序开发者必备技巧。如果不会调试,自己写的程序一旦出问题,往往无从下手。本人总结10年使用VC经验,对调试技巧做一个粗浅的介绍。希望对大家有所帮助。
  今天简单的介绍介绍调用堆栈。调用堆栈在我的专栏的文章VC调试入门提了一下,但是没有详细介绍。
  首先介绍一下什么叫调用堆栈:假设我们有几个函数,分别是function1,function2,function3,funtion4,且function1调用function2,function2调用function3,function3调用function4。在function4运行过程中,我们可以从线程当前堆栈中了解到调用他的那几个函数分别是谁。把函数的顺序关系看,function4、function3、function2、function1呈现出一种“堆栈”的特征,最后被调用的函数出现在最上方。因此称呼这种关系为调用堆栈(call
  当故障发生时,如果程序被中断,我们基本上只可以看到最后出错的函数。利用call stack,我们可以知道当出错函数被谁调用的时候出错。这样一层层的看上去,有时可以猜测出错误的原因。常见的这种中断时ASSERT宏导致的中断。
  在程序被中断时,debug工具条的右侧倒数第二个按钮一般是call stack按钮,这个按钮被按下后,你就可以看到当前的调用堆栈。
  实例一:介绍
  我们首先演示一下调用堆栈。首先我们创建一个名为Debug的对话框工程。工程创建好以后,双击OK按钮创建消息映射函数,并添加如下代码:
  void CDebugDlg::OnOK()
  // TODO: Add extra validation here
  ASSERT(FALSE);
  我们按F5开始调试程序。程序运行后,点击OK按钮,程序就会被中断。这时查看call stack窗口,就会发现内容如下:
  CDebugDlg::OnOK() line 176 + 34 bytes
  _AfxDispatchCmdMsg(CCmdTarget * 0x0012fe74 {CDebugDlg}, unsigned int 1, int 0, void (void)* 0x5f402a00 `vcall'(void), void * 0x, unsigned int 12, AFX_CMDHANDLERINFO * 0x) line 88
  CCmdTarget::OnCmdMsg(unsigned int 1, int 0, void * 0x, AFX_CMDHANDLERINFO * 0x) line 302 + 39 bytes
  CDialog::OnCmdMsg(unsigned int 1, int 0, void * 0x, AFX_CMDHANDLERINFO * 0x) line 97 + 24 bytes
  CWnd::OnCommand(unsigned int 1, long 656988) line 2088
  CWnd::OnWndMsg(unsigned int 273, unsigned int 1, long 656988, long * 0x0012f83c) line 1597 + 28 bytes
  CWnd::WindowProc(unsigned int 273, unsigned int 1, long 656988) line 1585 + 30 bytes
  AfxCallWndProc(CWnd * 0x0012fe74 {CDebugDlg hWnd=???}, HWND__ * 0x, unsigned int 273, unsigned int 1, long 656988) line 215 + 26 bytes
  AfxWndProc(HWND__ * 0x, unsigned int 273, unsigned int 1, long 656988) line 368
  AfxWndProcBase(HWND__ * 0x, unsigned int 273, unsigned int 1, long 656988) line 220 + 21 bytes
  USER32! 77d48709()
  USER32! 77d487eb()
  USER32! 77d4b368()
  USER32! 77d4b3b4()
  NTDLL! 7c90eae3()
  USER32! 77d4b7ab()
  USER32! 77d7fc9d()
  USER32! 77d76530()
  USER32! 77d58386()
  USER32! 77d5887a()
  USER32! 77d48709()
  USER32! 77d487eb()
  USER32! 77d489a5()
  USER32! 77d489e8()
  USER32! 77d6e819()
  USER32! 77d65ce2()
  CWnd::IsDialogMessageA(tagMSG * 0x {msg=0x wp=0x lp=0x000f001c}) line 182
  CWnd::PreTranslateInput(tagMSG * 0x {msg=0x wp=0x lp=0x000f001c}) line 3424
  CDialog::PreTranslateMessage(tagMSG * 0x {msg=0x wp=0x lp=0x000f001c}) line 92
  CWnd::WalkPreTranslateTree(HWND__ * 0x, tagMSG * 0x {msg=0x wp=0x lp=0x000f001c}) line 2667 + 18 bytes
  CWinThread::PreTranslateMessage(tagMSG * 0x {msg=0x wp=0x lp=0x000f001c}) line 665 + 18 bytes
  CWinThread::PumpMessage() line 841 + 30 bytes
  CWnd::RunModalLoop(unsigned long 4) line 3478 + 19 bytes
  CDialog::DoModal() line 536 + 12 bytes
  CDebugApp::InitInstance() line 59 + 8 bytes
  AfxWinMain(HINSTANCE__ * 0x, HINSTANCE__ * 0x, char * 0x00141f00, int 1) line 39 + 11 bytes
  WinMain(HINSTANCE__ * 0x, HINSTANCE__ * 0x, char * 0x00141f00, int 1) line 30
  WinMainCRTStartup() line 330 + 54 bytes
  KERNEL32! 7c816d4f()
  这里,CDebugDialog::OnOK作为整个调用链中最后被调用的函数出现在call stack的最上方,而内核中程序的启动函数Kernel32! 7c816d4f()则作为栈底出现在最下方。
  实例二:学习处理方法
  微软提供了MDI/SDI模型提供文档处理的建议结构。有些时候,大家希望控制某个环节。例如,我们希望弹出自己的打开文件对话框,但是并不想自己实现整个文档的打开过程,而更愿意MFC完成其他部分的工作。可是,我们并不清楚MFC是怎么处理文档的,也不清楚如何插入自定义代码。
  幸运的是,我们知道当一个文档被打开以后,系统会调用CDocument派生类的Serialize函数,我们可以利用这一点来跟踪MFC的处理过程。
  我们首先创建一个缺省的SDI工程Test1,并在CTest1Doc::Serialize函数的开头增加一个断点,运行程序,并打开一个文件。这时,我们可以看到调用堆栈是(我只截取了感兴趣的一段):
  CTest1Doc::Serialize(CArchive & {...}) line 66
  CDocument::OnOpenDocument(const char * 0x0012f54c) line 714
  CSingleDocTemplate::OpenDocumentFile(const char * 0x0012f54c, int 1) line 168 + 15 bytes
  CDocManager::OpenDocumentFile(const char * 0x0042241c) line 953
  CWinApp::OpenDocumentFile(const char * 0x0042241c) line 93
  CDocManager::OnFileOpen() line 841
  CWinApp::OnFileOpen() line 37
  _AfxDispatchCmdMsg(CCmdTarget * 0x class CTest1App theApp, unsigned int 57601, int 0, void (void)* 0x CWinApp::OnFileOpen, void * 0x, unsigned int 12, AFX_CMDHANDLERINFO * 0x) line 88
  CCmdTarget::OnCmdMsg(unsigned int 57601, int 0, void * 0x, AFX_CMDHANDLERINFO * 0x) line 302 + 39 bytes
  CFrameWnd::OnCmdMsg(unsigned int 57601, int 0, void * 0x, AFX_CMDHANDLERINFO * 0x) line 899 + 33 bytes
  CWnd::OnCommand(unsigned int 57601, long 132158) line 2088
  CFrameWnd::OnCommand(unsigned int 57601, long 132158) line 317
  从上面的调用堆栈看,这个过程由一个WM_COMMAND消息触发(因为我们用菜单打开文件),由CWinApp::OnFileOpen最先开始实际处理过程,这个函数调用CDocManager::OnFileOpen打开文档。
  我们首先双击CWinApp::OnFileOpen() line 37打开CWinApp::OnFileOpen,它的处理过程是:
   ASSERT(m_pDocManager != NULL);
   m_pDocManager-&OnFileOpen();
  m_pDocManager是一个CDocManager类的实例指针,我们双击CDocManager::OnFileOpen行,看该函数的实现:
  void CDocManager::OnFileOpen()
   // prompt the user (with all document templates)
   CString newN
   if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,
   OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, TRUE, NULL))
   // open cancelled
   AfxGetApp()-&OpenDocumentFile(newName);
   // if returns NULL, the user has already been alerted
  很显然,该函数首先调用DoPromptFileName函数来获得一个文件名,然后在继续后续的打开过程。
  顺这这个线索下去,我们一定能找到插入我们文件打开对话框的位置。由于这不是我们研究的重点,后续的分析我就不再详述。
  实例三:内存访问越界
  在Debug版本的VC程序中,程序会给每块new出来的内存,预留几个字节作为越界检测之用。在释放内存时,系统会检查这几个字节,判断是否有内存访问越界的可能。
  我们借用前一个实例程序,在CTest1App::InitInstance的开头添加以下几行代码:
   char * p = new char[10];
   memset(p,0,100);
   delete []p;
   return FALSE;
  很显然,这段代码申请了10字节内存,但是使用了100字节。我们在memset(p,0,100);这行加一个断点,然后执行程序,断点到达后,我们观察p指向的内存的值(利用Debug工具条的Memory功能),可以发现它的值是:
   CD CD CD CD CD CD CD CD
   CD CD FD FD FD FD FD FD
   00 00 00 00 00 00 00 00
   ......
  根据经验,p实际被分配了16个字节,后6个字节用于保护。我们按F5全速执行程序,会发现如下的错误信息被弹出:
   Debug Error!
   Program: c:\temp\test1\Debug\test1.exe
   DAMAGE: after normal block (#55) at 0x00421AB0
   Press Retry to debug the application
  该信息提示,在正常内存块0x00421AB0后的内存被破坏(内存访问越界),我们点击Retry进入调试状态,发现调用堆栈是:
  _free_dbg_lk(void * 0x00421ab0, int 1) line 1033 + 60 bytes
  _free_dbg(void * 0x00421ab0, int 1) line 970 + 13 bytes
  operator delete(void * 0x00421ab0) line 351 + 12 bytes
  CTest1App::InitInstance() line 54 + 15 bytes
  很显然,这个错误是在调用delete时遇到的,出现在CTest1App::InitInstance() line 54 + 15 bytes之处。我们很容易根据这个信息找到,是在释放哪块内存时出现问题,之后,我们只需要根据这个内存的访问过程确定哪儿出错,这将大大降低调试的难度。
  实例四:子类化
  子类化是我们修改一个现有控件实现新功能的常用方法,我们借用实例一中的Debug对话框工程来演示我过去学习子类化的一个故事。我们创建一个缺省的名为Debug的对话框工程,并按照下列步骤进行实例化:
  在对话框资源中增加一个Edit控件
  用class wizard为CEdit派生一个类CMyEdit(由于今天不关心子类化的具体细节,因此这个类不作任何修改)
  为Edit控件,增加一个控件类型变量m_edit,其类型为CMyEdit
  在OnInitDialog中增加如下语句:
  m_edit.SubclassDlgItem(IDC_EDIT1,this);
  我们运行这个程序,会遇到这样的错误:
  Debug Assertion Failed!
  Application:C:\temp\Debug\Debug\Debug.exe
  File:Wincore.cpp
  Line:311
  For information on how your program can cause an assertion failure, see Visual C++ documentation on asserts.
  (Press Retry to debug the application)
  点击Retry进入调试状态,我们可以看到调用堆栈为:
  CWnd::Attach(HWND__ * 0x) line 311 + 28 bytes
  CWnd::SubclassWindow(HWND__ * 0x) line 3845 + 12 bytes
  CWnd::SubclassDlgItem(unsigned int 1000, CWnd * 0x0012fe34 {CDebugDlg hWnd=0x001d058a}) line 3883 + 12 bytes
  CDebugDlg::OnInitDialog() line 120
  可以看出在Attach句柄时出现问题,出问题行的代码为:
   ASSERT(m_hWnd == NULL);
  这说明我们在子类化时不应该绑定控件,我们删除CDebugDialog::DoDataExchange中的下面一行:
   DDX_Control(pDX, IDC_EDIT1, m_edit);
  问题就得到解决
  简而言之,call stack是调试中必须掌握的一个技术,但是程序员需要丰富的经验才能很好的掌握和使用它。你不仅仅需要熟知C++语法,还需要对相关的平台、软件设计思路有一定的了解。我的文章只能算一个粗浅的介绍,毕竟我在这方面也不算高手。希望对新进有一定的帮助。
  调试之编程准备
  对于一个程序员而言,学习一种语言和一种算法是非常容易的(不包括那些上学花很多时间玩,上班说学习没时间的人)。但是,任何程序都可能是有瑕疵的,尤其有过团队协作编程经验的人,对这个感触尤为深刻。
  在我前面的述及调试的文章里,我侧重于VC集成环境中的一些设置信息和调试所需要的一些基本技巧。但是,仅仅知道这些是不够的。一个成功的调试的开端是编程中的准备。
  分离错误
  很多程序员喜欢写下面这样的式子:
   CLeftView* pView =
   ((CFrameWnd*)AfxGetApp()-&m_pMainWnd)-&m_wndSplitterWnd.GetPane(0,0);
  如果一切顺利,这样的式子当然是没什么问题。但是作为一个程序员,你应该时刻记得任何一个调用在某些特殊的情况下都可能失败,一旦上面某个式子失败,那么整个级联式就会出问题,而你很难弄清楚到底哪儿出错了。这样的式子的结果往往是:省了2分钟编码的时间,多了几星期的调试时间。
  对于上面的式子,应该尽可能的把式子分解成独立的函数调用,这样我们可以随时确定是哪个函数调用出问题,进口缩小需要检查的范围。
  检查返回值
  检查返回值对于许多编程者来说似乎是一个很麻烦的事情。但是如果你能在每个可能出错的函数调用处都检查返回值,就可以立刻知道出错的函数。
  有些人已经意识到检查返回值的重要性,但是要记住,只检查函数是否失败是不够的,我们需要知道函数失败的确切原因。例如下面的代码:
  if(connect(sock, (const sockaddr*)&addr,sizeof(addr)) == SOCKET_ERROR)
   AfxMessageBox(&connect failed&);
  尽管这里已经检查了返回值,实际上没有多少帮助。正如很多在vckbase上提问的人一样,大概这时候只能喊“为什么连接失败啊?”。这种情况下,其实只能猜测失败的原因,即使高手,也无法准确说出失败的原因。
  增加诊断信息
  在知道错误的情况下,应该尽可能的告诉测试、使用者更多的信息,这样才能了解导致失败的原因。如果程序员能提供如下错误信息,对于诊断错误是非常有帮助的:
  出错的文件:我们可以借助宏THIS_FILE和__FILE__。注意THIS_FILE是在cpp文件手工定义的,而__FILE__是编译器定义的。当记录错误的函数定义在.h中时,有时候用THIS_FILE更好,因为他能说明在哪个cpp中调用并导致失败的。
  出错的行:我们可以借助宏__LINE__
  出错的函数:如果设计的好,有以上两项已经足够。当然我们可以直接打印出出错的函数或者表达式,这样在大堆代码中搜索(尤其是不支持go to line的编辑器中)还是很有用的。大家可以参见我的文章/arong/archive//14704.html中的方式进行处理,也许是一个基本的开端。
  出错的原因:出错的原因很多只能由程序自己给出。如果出错只会问别人,那么你永远不可能成为一个合格的程序设计人员。很多函数失败时都会设置errno。我们可以用GetLastError获得错误码,并通过FormatMessage打印出具体错误的文字描述。
(4) vc可以调试多线程吗?
&&&& 答:&&& 可以。
&&&&&&&&&&&&&&&a)首先单击【F10】,开启文件的调试状态
&&&&&&&&&&&&& &b)单击【Debug】菜单,选择【thread】子菜单
&&&&&&&&&&&& & c) 在这里,你可以看到当前所有创建的线程,如果你需要对某一个线程进行调试,那么先把你需要调试的线程设置为Set Focus状态,另外一个thread设置为suspend状态,与此同时resume你需要调试的thread即可。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:62570次
排名:千里之外
原创:15篇
转载:26篇
评论:13条
(1)(5)(1)(1)(2)(3)(6)(1)(4)(3)(2)(5)(7)信息标识和处理(7)
本节我们重点讨论栈指针esp和帧指针ebp,围绕这两个重要的寄存器,推导出函数栈帧结构。
一:压栈和出栈的操作本质
& & & & 上一节我们了解到push和pop是汇编中压栈和出栈的指令。栈这个东东,当某个程序运行时,会划分一个块固定大小的区域(存储器映射),而栈就属于这个区域的一部分。要了解出入栈首先要了解栈的结构:
& & & 地址 & & 栈中内容
&数据(栈底)&&
数据1(栈顶)
(新栈顶)
& & & & 从上图看出,栈的增长方向是向下的。栈有个最大地址,这个地址成为栈底,也是存储栈里面存储第一个元素的位置,随着入栈个数增加,栈顶的地址不断减小。
& & & & esp寄存器就是专门用来存储栈顶地址的。在汇编中,%esp读出栈顶地址,(%esp)就能读出栈顶里的数值。如上图所示,如果再进行一次入栈push操作时,那么栈顶%esp就跳到地址0xFC(0x100-4)处,新压的数据也会存在这个地址上。如果上图不执行push,而是直接执行pop出栈时,esp将存储地址0x104。
& & & & push和pop这两个汇编操作指令,是可以用基本的汇编操作代替的,事实上,push和pop在汇编中对应的操作是:
push %ebp:subl$4, %esp
& & & movl
%ebp, (%esp)
pop & %eax:movl & (%esp), &%eax
& & & & 在分析上面汇编代码之前再复习一下,%eax直接获取里面的值,(%eax)类似C指针‘*’间接寻址操作,是取出%eax里的值作为地址来看,再根据这个地址找到相应位置,并取出其中的值。
& & & & 还以上图为例。先来看push压栈,压栈是增加栈的元素,由于有新的数据(ebp里的值为数据0,具体什么值先不关心)要入栈,而栈又是向下生长的,因此需要将存有栈顶地址信息的esp进行调整,具体操作是将esp减4,得到增长后的下一个栈顶地址,subl$4, %esp操作使得esp的值从0x100跳变到0xFC,实现了栈顶的生长;接着是赋值,我们需要把ebp里的值传送到新的栈顶指向的空间中去(地址0xFC代表的空间),完成入栈。语句movl
&%ebp, (%esp)比较好理解,就是把ebp里的值,通过“()”对栈指针进行间接引用,传送到地址0xFC的空间里面去,esp是栈指针(叫栈顶指针更好理解)。
& & & & 为啥%esp要加括号?如果不加括号,栈指针所存的地址数据将被破坏,本来跳变好了新栈顶地址0xFC,会因为你的一个不加括号的语句而使栈指针%esp被覆盖成%ebp的值(数据0)。而加了括号,则会做间接寻址操作,通过%esp,找到地址为0xFC的空间(也就是新的栈顶空间),并把数据0成功传送进去。
& & & & 一旦你理解了上面冗长的废话,再理解pop就很简单了,出栈无非就是把操作反过来。比如刚才push完了,我们再执行pop %eax,就是要把栈顶元素的值弹出来,传送到%eax中去,然后栈顶更新状态。那么movl & (%esp), &%eax语句就是将当前栈顶里的值(数据0),传送到eax中去;而addl&&$4, &%esp就是更新栈指针,把地址值加回去(从0xFC变回0x100)。
& & & & 这里有个细节问题,关于出栈,有没有发现,只有数据出和栈顶更新,并没有数据删除操作。也就是说,刚才连续执行了push %ebp和pop %eax后,栈指针指向的是0x100地址,栈顶的值是数据1。那么地址0xFC里存的什么呢?答案当然是数据0,因为没有任何语句删除它,所以才会出现有时候你调试C语言程序,指针越界访问后,会读出一些已经失效函数里面的临时变量值,就是这个原因。
& & & & 用汇编语句理解出栈入栈,对于接下来的函数栈空间的理解是至关重要的。
二:函数调用的栈帧结构
& & & & 在我看来,从某种意义上说,C语言就是个函数嵌套语言,从一个主函数开始,内部生出几个子函数,每个子函数下面还有更细的子函数,有时父子函数之间还会出现递归嵌套调用,在加上循环和条件判断,如此复杂的操作,编译器是怎么翻译成汇编来实现的?这依赖于简单实用的栈帧结构,这里我们引用网上的一个火图:
& & & & 说句老实话,本来这个图并不是那么难理解的,无论函数嵌套有多复杂,总有个先后吧?这个帧那个帧不就是根据调用的先后排列顺序的,先调用的函数,其栈帧结构就整体先入栈,后调用的函数就后入栈,那么栈顶所代表的函数帧(当前帧),就是当前正在调用的函数,所需要的数据映射,解释完毕。
& & & & 如果栈帧结构真这么简单,那每个人都只需要花阅读上面文字所需要的时间,就能搞明白了。栈帧结构最难搞懂的,就是那句“被保存的%ebp”,这句话难的背后,是对ebp在栈帧中作用的理解,可以这样的说,只有你理解的ebp,才能真正理解栈帧结构,你甚至可以当黑客,往栈帧里嵌入恶意代码,构造自己的栈帧,这种小游戏前段时间我自己也尝试过,把生成好的a.out可执行文件,用vi直接修改二进制,加入恶意代码。程序原本要执行打印“I am
Superman:)”的函数,经过对可执行文件的直接修改,a.out乖乖的跳转到另一个函数,打印出“I am Hacker!^_^”,而Superman已不知去向,这个就是典型的利用缓冲区溢出进行代码攻击,虽然显得太小儿科,但原理类似。
& & & & 言归正传。要理解%ebp,首先还是要复习一下上面讲的间接引用,搞清楚寄存器所存值的概念。寄存器里存的值本质上就是数值,关键是我们如何看待它的意义,就比如栈指针%esp,叫它栈指针是因为它一般来说存的都是某个空间的地址,这是编译器的习惯分配。如果你是做编译器的,完全可以用%esp当成%eax或者其他什么寄存器来临时存放一下其他数值,再把地址赋回值给它,如果不嫌麻烦的话。因此类似栈帧结构的这些知识,其实是编译器事先定义好的对寄存器的使用规则,记住,寄存器里的值我们要怎么理解,那是由编译器说了算的。
& & & & 为了简单好理解,我们讨论最简单的函数嵌套,假如函数grand调用函数father,而father调用函数son,father的栈帧就是上图所说的“调用者的帧”,而son就是“当前帧”,grand自然就包含在“较早的帧”之中。father有1~n,n个变量要作为参数传给son。从上图能明显看出,n个参数是倒着排列的,这是由栈结构决定。在参数传递中,son(1,2,3,…,n)代码顺序,在栈帧结构上是地址由小增大排列的。参数下面是返回地址,这个返回地址,其实就是father函数自己的地址,同时也是father函数栈帧的末尾(注意和栈顶或者栈底概念完全无关)。
& & & & 好,回过头来看,那么参数n以上的省略号是什么呢?其实,son函数栈帧的“参数构造区域”,和father的参数1~n是一回事,也许里面放着的是参数1~m,用于son来调用孙子函数时用,因此参数n上面的省略号,就是father函数被保存的寄存器、本地变量和临时变量,再往上就是father函数自己的“被保存的%ebp”。
再往上呢?就进入grand函数的栈帧结构(较早的栈帧),往上第一个一定也是“返回地址”,其实就是grand函数执行完father后应该继续执行的代码的地址。
& & & & 说到这里可能你觉得还好,按照调用顺序,函数的栈帧结构维护得很清楚。可以想象,当某个函数要调用其他函数调用时,先通过一系列压栈操作,在栈里面备份函数自身的本地临时变量,还有传递给子函数的参数变量信息,最后压上函数自个儿的地址,完事,下面的空间就留给子函数玩了。
& & & & 这里问题就来了,CPU如何区分不同的栈帧?如何搞清楚栈里面哪部分是子函数哪部分是父函数?栈指针%esp只知道自己现在在哪玩,对于具体玩的是哪个函数的内容,那是一头雾水啊。于是我们有必要解开%ebp面纱了。
三:神秘的%ebp
& & & & %ebp叫帧指针,相信熟悉C指针的朋友看到名字时,对%ebp的工作原理就基本明白个七八分了。没错,既然叫帧指针,那就是用来存放各帧首地址的指针。
& & & & 设想,当father函数要调用son函数时,需要对栈帧信息进行修改和维护,如何在son函数执行完后让CPU顺利的找到father的栈帧地址并成功返回呢?这就要在调用son之前做好充分的准备工作。比如,father栈帧有自己的帧首,在father函数执行时,%ebp就保存了这个帧首的地址值,或者说%ebp正指向帧首。当调用子函数son时,%ebp就会保存son的帧首地址,为了让son在返回时能够顺利更新%ebp,使得帧指针顺利指回到father的帧来,有必要在%ebp指向son帧首的同时,更改帧首空间内所保存的值为father帧首地址,也就是son的所谓“保存的%ebp”,或者说旧的%ebp值,父函数调用时%ebp的值。
& & & & 这里感觉很绕的同学,一定是指针基础还不够扎实。来区分下一个概念,一个存储单元空间,有两个属性:1、CPU访问这个存储单元需要依赖的地址值;2、这个存储单元所存储的数值,空间地址值和空间内存储的数值,区分这两个值是理解指针概念的基础。现在讨论函数的帧,每个帧都帧首,帧首作为存储单元空间,当然有标识自己的空间地址,同时空间里存了一个数值。栈帧结构恰恰巧妙的利用了这种概念,让%ebp始终保存当前调用函数的帧首地址,而当前帧首内又存储着父函数的帧首地址,以此类推,每一个当前调用函数的帧首内都保留着父函数的帧首地址,函数执行完成时都能顺利更新栈指针%ebp的值,一直可以推到main函数的帧首,通过栈指针%ebp的修改和被保存,就能确保栈帧结构的访问顺利进行,是不是很奇妙?
& & & & 以上是纯理论推导,一旦你真正看明白,那具体的汇编代码实现就会很容易弄懂了。函数调用在汇编中还会涉及到call、leave、ret等指令,其实都可以用更基本的指令进行描述。
& & & & 为了便于讲解,我写了一段简单得不能再简单的函数调用事例ebpesp.c:
int son_add(int a, int b)
& & & & return a+b;
int father()
& & & & int a = 8;
& & & & int b = 9;
& & & & int sum = 0;
& & & & sum = son_add(a, b);
& & & & 利用gcc 的-O2优化选项进行编译生成ebpesp.o的二进制文件(没有main函数所有不能编译成可执行文件,但汇编原理完全一样)
& & & & 然后再反汇编代码,其中函数体部分如下:
&son_add&:
& &0: & 55 & & & & & & & & & & &push & %ebp
& &1: & 89 e5 & & & & & & & & mov & &%esp,%ebp
& &3: & 8b 45 0c & & & & & &mov & &0xc(%ebp),%eax
& &6: & 03 45 08 & & & & & &add & &0x8(%ebp),%eax
& &9: & c9 & & & & & & & & & & &leave &
& &a: & c3 & & & & & & & & & & &ret & &
& &b: & 90 & & & & & & & & & & &nop & &
0000000c &father&:
& &c: & 55 & & & & & & & & & & &push & %ebp
& &d: & 89 e5 & & & & & & & & mov & &%esp,%ebp
& &f: & 6a 02 & & & & & & & & &push & $0x9
& 11: & 6a 01 & & & & & & & &push & $0x8
& 13: & e8 fc ff ff ff & & & &call & 14 &father+0x8&
& 18: & 5a & & & & & & & & & & pop & &%edx
& 19: & 59 & & & & & & & & & & pop & &%ecx
& 1a: & c9 & & & & & & & & & & leave &
& 1b: & c3 & & & & & & & & & & ret & &
& & & & father函数从第三行push开始看起。两条push语句明显就是对参数进行压栈,先压9后压8,与c语言中的自右向左的原理一致,两个参数的值被成功压入栈。注意此时还是father执行阶段,因此参数所压的位置仍属于father的栈帧空间。接下来就是子函数调用call语句,call可以近似看成做如下操作:
call:push 返回地址,%esp
& 子函数地址
& & & & 因此father中的call就可以翻译成更直观的汇编语句就是:(注意,18和0都是逻辑地址,这里只是为举例而写的伪汇编代码,在后面章节将详细描述。)
push $18,%esp
& & & & 可见,两个参数入栈完成后,接着就是father函数的返回地址,返回到18这个地址,以便继续father代码的执行。到此为止,father函数的栈帧维护结束,函数调用的准备工作完成,可以通过跳转指令jmp跳转到son_add函数了。
& & & & 我们发现,son_add第一句是push %ebp,理解这句很关键。想想,在这条语句之前,程序运行的是father函数,那么%ebp自然也保存的是father函数的帧首地址,直到执行到0,也没有谁修改过它,因此在还行push %ebp时,%ebp里仍然保存的是father帧首地址,现在对他进行压栈,于是push & %ebp就使得该帧首地址就被顺利的放进了“返回地址”单元的下面(成为新栈顶,%esp就存储了其地址值),再由于这是运行son_add函数的第一条语句,因此该栈顶就作为son_add的帧首了,此时该帧首里面到是舒服的躺着father帧首的地址值,%ebp却并没有指向son_add函数的帧首,因此mov
&%esp,%ebp就是把当前这个帧首的地址值赋给%ebp,于是在son_add函数返回前,%ebp都作为当前帧指针不会变动了。
& & & & 接下来的两句很有意思,mov & &0xc(%ebp),%eax是对帧指针里的地址先增加0xC再取里面的值,增加12是啥意思?12刚好是4的倍数,也就是向上移动三个栈存储单元。根据栈结构图发现,%ebp作为帧首,向上移动一个单元是“返回地址”;向上移动两个单元是参数1,向上移动三个单元当然是参数2!也就是我们传给son_add的第二个参数9。因此这条汇编的意思是把9赋值给%eax寄存器。依次类推是不是还应该把参数1的这个8赋给另一个寄存器呢?编译器可没这么傻,你son_add不就是想做个加法么?直接add
& &0x8(%ebp),%eax,让%ebp寻找到参数1的地址位置,读取出8,然后直接和%eax的值相加,搞定!
& & & &&好了,这个时候%eax寄存器就是存有加法结果的寄存器了,计算完成子函数需要返回了,于是先后执行leave和ret,先看leave的等价汇编代码:
leave:movl %ebp,%esp
& & pop & %ebp
& & & &&这步在理解上稍显困难,主要是对出入栈的操作理解。movl %ebp,%esp这条语句,其实目的就是破坏子函数son_add栈帧结构。想想看,直接修改栈指针%esp,让他指向son_add的帧首,然后执行pop & %ebp,将帧首里的值赋给%ebp!回忆下帧首里存的是啥值啊?那当然是father帧首的地址值啊,这句目的就是让%ebp重新指回father栈帧的帧首!OK,son_add的帧首被弹出栈后,栈指针也不会再指向son_add帧首了,而是指向他的上面一个栈存储单元,那就是father帧的末尾:返回地址,leave的使命便完成。接下来就是ret,考虑到ret要完成函数调用的返回,还要维护栈帧的返回,我们可以猜测ret的等价汇编代码应该是:
& & & & add &$4, %esp
& & & &&因为此时%esp指向father帧的末尾,而该末尾里面又存储了father调用son_add函数后应该返回的地址(这里应该是18),因此就应该是将该地址取出,直接跳转到18,也就是call语句之后的语句,而子函数son_add的调用既然已经完成,根据个人的猜测,“返回地址”已失去意义,因此栈指针会加4返回。
& & & &&至此,关于函数调用的栈帧原理就全部讲完了,如果你看懂了上面的论述,就能自然而然的推到出father其余部分的汇编含义,也能显而易见的弄明白大型C程序中每个函数都已push & %ebp、mov & &%esp,%ebp开头以及leave和ret结尾,层层包裹,稳定和完美。
& & & &&我已经使出近乎手把手式的讲解和冗长的反复叙述,这绝对是迄今为止最傻瓜式的栈帧原理阐述了。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:7332次
排名:千里之外
原创:17篇
(1)(1)(1)(1)(1)(2)(2)(5)(4)

我要回帖

更多关于 苹果7pius多少钱 的文章

 

随机推荐