2.2 函数调用与栈机制

linux C用户态调试追踪函数调用堆栈以及定位段错误

linux C用户态调试追踪函数调用堆栈以及定位段错误 一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。 在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。 int backtrace(void **buffer,int size) 该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小 在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址 注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容 char ** backtrace_symbols (void *const *buffer, int size) backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值) 函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址 现在,只有使用ELF二进制格式的程序才能获取函数名称和偏移地址.在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的符号给链接器,以能支持函数名功能(比如,在使用GNU ld链接器的系统中,你需要传递(-rdynamic),-rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果你的链接器支持-rdynamic的话,建议将其加上!) 该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针. 注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL void backtrace_symbols_fd (void *const *buffer, int size, int fd)

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

函数调用过程分析 刘兵QQ: 44452114 E-mail: liubing2000@https://www.360docs.net/doc/4d9533251.html, 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为栈底,

C语言函数参数入栈的理解分析

先来看这样一段程序: [cpp]view plain copy print? 1.#include 2.#include 3.#include 4. 5.void print1(int a,int b,int c) 6.{ 7. printf("%p\n",&a); 8. printf("%p\n",&b); 9. printf("%p\n",&c); 10.} 11. 12.int main(void) 13.{ 14. print1(1,2,3); 15. exit(0); 16.} 它的输出是: [cpp]view plain copy print? 1.0022FF40 2.0022FF44 3.0022FF48 发现a,b,c的地址是逐渐增大的,差值是4个字节。这和我所知道的:C函数参数入栈的顺序是从右到左是相匹配的,而且地址的增大值也 与变量所占的字节数相匹配。 不过当把程序稍微做一下修改,如下: [cpp]view plain copy print? 1.#include

2.#include 3.#include 4. 5.void print2(char a,char b,char c) 6.{ 7. printf("%p\n",&a); 8. printf("%p\n",&b); 9. printf("%p\n",&c); 10.} 11. 12.int main(void) 13.{ 14. print2(1,2,3); 15. exit(0); 16.} 再观察一下它的输出: [cpp]view plain copy print? 1.0022FF2C 2.0022FF28 3.0022FF24 怎么和上面的效果是相反的!虽然我知道这肯定编译器的一个技巧,不过参数入栈的顺序是从右到左的概念却动摇了。 为了弄清楚其中的道理,必须观察程序生成的中间.s文件,为此,我执行了以下一条命令: [cpp]view plain copy print? 1.gcc -S test.c(当前C文件中保存的程序是文章一开始的那个) 在当前目录下生成test.s 文件 使用vim打开test.s文件(只截取主要内容了): esp是指向栈顶的指针,ebp是用来备份这个指针的。栈的形状如下: esp

Excel中三个查找引用函数的用法(十分有用)

在Excel中,我们经常会需要从某些工作表中查询有关的数据复制到另一个工作表中。比如我们需要把学生几次考试成绩从不同的工作表中汇总到一个新的工作表中,而这几个工作表中的参考人数及排列顺序是不完全相同的,并不能直接复制粘贴。此时,如果使用Excel的VLOOKUP、INDEX或者OFFSET函数就可以使这个问题变得非常简单。我们以Excel 2007为例。 图1 假定各成绩工作表如图 1所示。B列为,需要汇总的项目“总分”及“名次”位于H列和I列(即从B列开始的第7列和第8列)。而汇总表则如图2所示,A列为列,C、D两列分别为要汇总过来的第一次考试成绩的总分和名次。其它各次成绩依次向后排列。

图2 一、 VLOOKUP函数 我们可以在“综合”工作表的C3单元格输入公式“=VLOOKUP($B3,第1次!$B$1:$I$92,7,FALSE)”,回车后就可以将第一位同学第一次考试的总分汇总过来了。 把C3单元格公式复制到D3单元格,并将公式中第三个参数“7”改成“8”,回车后,就可以得到该同学第一次考试名次。 选中C3:D3这两个单元格,向下拖动填充句柄到最后就可以得到全部同学的总分及名次了。是不是很简单呀?如图3所示。

VLOOKUP函数的用法是这样的:VLOOKUP(参数1,参数2,参数3,参数4)。“参数1”是“要查找谁?”本例中B3单元格,那就是要查找B3单元格中显示的人名。“参数2”是“在哪里查找?”本例中“第1次!$B$1:$I$92”就是告诉Excel在“第1次”工作表的B1:I92单元格区域进行查找。“参数3”是“找第几列的数据?”本例中的“7”就是指从“第1次”工作表的B列开始起,第7列的数据,即H列。本例中“参数4”即“FALSE”是指查询方式为只查询精确匹配值。 该公式先在“第1次”工作表的B!:I92单元格区域的第一列(即B1:B92单元格区域)查找B3单元格数据,找到后,返回该数据所在行从B列起第7列(H列)的数据。所以,将参数3改成“8”以后,则可以返回I列的数据。 由此可以看出,使用VLOOKUP函数时,参数1的数据必须在参数2区域的第一列中。否则是不可以查找的。 二、INDEX函数 某些情况下,VLOOKUP函数可能会无用武之地,如图4所示。“综合”工作表中,列放到了A 列,而B列要求返回该同学所在的班级。但我们看前面的工作表就知道了,“班级”列是位于“”列前面的。所以,此时我们不可能使用VLOOKUP函数来查找该同学的班级。而INDEX函数就正可以一试身手。

程序调试技巧之调用堆栈

调试技巧之调用堆栈 在计算机科学中,Callstack是指存放某个程序的正在运行的函数的信息的栈。Call stack由stack frames组成,每个stack frame对应于一个未完成运行的函数。 在当今流行的计算机体系架构中,大部分计算机的参数传递,局部变量的分配和释放都是通过操纵程序栈来实现的。栈用来传递函数参数,存储返回值信息,保存寄存器以供恢复调用前处理机状态。每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做stack frame,也就是说,stack frame这个说法主要是为了描述函数调用关系的。 Stackframe组织方式的重要性和作用体现在两个方面:第一,它使调用者和被调用者达成某种约定。这个约定定义了函数调用时函数参数的传递方式,函数返回值的返回方式,寄存器如何在调用者和被调用者之间进行共享;第二,它定义了被调用者如何使用它自己的stack frame来完成局部变量的存储和使用。 简单介绍 调试是程序开发者必备技巧。如果不会调试,自己写的程序一旦出问题,往往无从下手。本人总结10年使用VC经验,对调试技巧做一个粗浅的介绍。希望对大家有所帮助。 今天简单的介绍介绍调用堆栈。调用堆栈在我的专栏的文章VC调试入门提了一下,但是没有详细介绍。 首先介绍一下什么叫调用堆栈:假设我们有几个函数,分别是function1,function2,function3,funtion4,且function1调用function2,function2调用function3,function3调用function4。在function4运行过程中,我们可以从线程当前堆栈中了解到调用他的那几个函数分别是谁。把函数的顺序关系看,function4、function3、function2、function1呈现出一种“堆栈”的特征,最后被调用的函数出现在最上方。因此称呼这种关系为调用堆栈(callstack)。 当故障发生时,如果程序被中断,我们基本上只可以看到最后出错的函数。利用call stack,我们可以知道当出错函数被谁调用的时候出错。这样一层层的看上去,有时可以猜测出错误的原因。常见的这种中断时ASSERT宏导致的中断。 在程序被中断时,debug工具条的右侧倒数第二个按钮一般是callstack按钮,这个按钮被按下后,你就可以看到当前的调用堆栈。

C语言函数调用栈

C语言函数调用栈(一) 程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。函数调用过程通常使用堆栈实现,每个用户态进程对 应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄 存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。 不同处理器和编译器的堆栈布局、函数调用方法都可能不同,但堆栈的基本概念是一样 的。 1 寄存器分配 寄存器是处理器加工数据或运行程序的重要载体,用于存放程序执行中用到的数据和指令。因此函数调用栈的实现与处理器寄存器组密切相关。 Intel 32位体系结构(简称IA32)处理器包含8个四字节寄存器,如下图所示: 图1 IA32处理器寄存器 最初的8086中寄存器是16位,每个都有特殊用途,寄存器名城反映其不同用途。由于 IA32平台采用平面寻址模式,对特殊寄存器的需求大大降低,但由于历史原因,这些寄存 器名称被保留下来。在大多数情况下,上图所示的前6个寄存器均可作为通用寄存器使用。某些指令可能以固定的寄存器作为源寄存器或目的寄存器,如一些特殊的算术操作指令 imull/mull/cltd/idivl/divl要求一个参数必须在%eax中,其运算结果存放在%edx(higher 32-bit)和%eax (lower32-bit)中;又如函数返回值通常保存在%eax中,等等。为避免兼容性问题,ABI规范对这组通用寄存器的具体作用加以定义(如图中所示)。 对于寄存器%eax、%ebx、%ecx和%edx,各自可作为两个独立的16位寄存器使用,而低16位寄存器还可继续分为两个独立的8位寄存器使用。编译器会根据操作数大小选择合 适的寄存器来生成汇编代码。在汇编语言层面,这组通用寄存器以%e(AT&T语法)或直接以e(Intel语法)开头来引用,例如mov $5, %eax或mov eax, 5表示将立即数5赋值给寄存器%eax。 在x86处理器中,EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指

C语言函数调用参数传递栈详解

C语言调用函数过程详解 Sunny.man 1.使用环境: gcc 版本4.1.2 20071124 (Red Hat 4.1.2-42) 2.示例源代码 int foo(int a,int b) { int a1=0x123; return a1+a+b; } int main() { foo(2,3); return 0; } 3.运行程序 命令:gdb a.out Start Disassemble 4.汇编函数清单 4.1main函数的汇编 0x0804836c : lea 0x4(%esp),%ecx 0x08048370 : and $0xfffffff0,%esp 0x08048373 : pushl 0xfffffffc(%ecx) 0x08048376 : push %ebp 0x08048377 : mov %esp,%ebp 0x08048379 : push %ecx 0x0804837a : sub $0x8,%esp 0x0804837d : movl $0x3,0x4(%esp) 0x08048385 : movl $0x2,(%esp) 0x0804838c : call 0x8048354 0x08048391 : mov $0x0,%eax 0x08048396 : add $0x8,%esp 0x08048399 : pop %ecx 0x0804839a : pop %ebp 0x0804839b : lea 0xfffffffc(%ecx),%esp 0x0804839e : ret

C语言函数调用规定

在C语言中,假设我们有这样的一个函数: int function(int a,int b) 调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。 栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。 函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。 在参数传递中,有两个很重要的问题必须得到明确说明: 当参数个数多于一个时,按照什么顺序把参数压入堆栈 函数调用后,由谁来把堆栈恢复原装 在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:stdcall cdecl fastcall thiscall naked call stdcall调用约定 stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall.在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK. stdcall调用约定声明的语法为(以前文的那个函数为例): int __stdcall function(int a,int b) stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸 以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成: push 2 第二个参数入栈 push 1 第一个参数入栈 call function 调用参数,注意此时自动把cs:eip入栈 而对于函数自身,则可以翻译为: push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复 mov ebp,esp 保存堆栈指针

[CC++] 函数调用的栈分配

[C/C++] 函数调用的栈分配 压栈,跳转到被调函数的入口点。 进入被调函数时,函数将esp减去相应字节数获取局部变量存储空间。被调函数返回(ret)时,将esp加上相应字节数,归还栈空间,弹出主调函数压在栈中的代码执行指针(eip),跳回主调函数。再由主调函数恢复到调用前的栈。 为了访问函数局部变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp 的值保存在ebp中,并事先将原ebp的值压栈保存,以声明中的顺序(即压栈的相反顺序)来确定偏移量。 访问函数的局部变量和访问函数参数的区别: 局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。 函数的返回值不同于函数参数,可以通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,将通过eax寄存器传递。如果返回值类型是64位变量,如_int64,则通过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是struct或class类型,编译器将通过隐式修改函数的签名,以引用型参数的形式传回。由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。基于这个原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。这种语法不兼容性是为了加强C++的类型安全,但同时也带来了一些代码兼容性问题。 代码示例 VarType Func (Arg1, Arg2, Arg3, ... ArgN) { VarType Var1, Var2, Var3, ...VarN; //... return VarN; } 假设sizeof(VarType) = 4(DWORD), 则一次函数调用汇编代码示例为: 调用方代码: push ArgN ; 依次逆序压入调用参数 push ... push Arg1 call Func_Address ; 压入当前EIP后跳转 跳转至被调方代码:

函数调用过程分析

1. 函数调用过程分析
1. 函数调用
我们用下面的代码来研究函数调用的过程。 例 19.1. 研究函数的调用过程
int bar(int c, int d) { int e = c + d; return e; }
int foo(int a, int b) { return bar(a, b); }
int main(void) { foo(2, 3); return 0; }
如果在编译时加上-g 选项(在第 10 章 gdb 讲过-g 选项),那么用 objdump 反汇编时可以把 C 代 码和汇编代码穿插起来显示,这样 C 代码和汇编代码的对应关系看得更清楚。反汇编的结果很长, 以下只列出我们关心的部分。
$ gcc main.c -g $ objdump -dS a.out ...

08048394 : int bar(int c, int d) { 8048394: 8048395: 8048397: 55 89 e5 83 ec 10 push mov sub %ebp %esp,%ebp $0x10,%esp
int e = c + d; 804839a: 804839d: 80483a0: 80483a2: 8b 55 0c 8b 45 08 01 d0 89 45 fc mov mov add mov 0xc(%ebp),%edx 0x8(%ebp),%eax %edx,%eax %eax,-0x4(%ebp)
return e; 80483a5: } 80483a8: 80483a9: c9 c3 leave ret 8b 45 fc mov -0x4(%ebp),%eax
080483aa :
int foo(int a, int b) { 80483aa: 80483ab: 80483ad: 55 89 e5 83 ec 08 push mov sub %ebp %esp,%ebp $0x8,%esp
return bar(a, b); 80483b0: 80483b3: 80483b7: 8b 45 0c 89 44 24 04 8b 45 08 mov mov mov 0xc(%ebp),%eax %eax,0x4(%esp) 0x8(%ebp),%eax

函数调用

杨振平

●函数定义后,并不能自动执行,必须通过函数调用来实现函数的功能。 ●函数调用,即控制执行某个函数。 ●C++中,主函数可以调用其它子函数,而其它函数之间也可以相互调用。 ●在本节中,我们将介绍一下内容: ?函数调用的格式 ?参数的传递方式 ?为形参指定默认值 ?数组名作函数参数 ?结构体变量作函数参数

函数调用的一般格式: <函数名>(<实际参数表>)//有参调用 或<函数名>()//无参调用 其中: ●<函数名>为要使用的函数的名字。 ●<实际参数表>是以逗号分隔的实参列表,必须放在一对圆括号中。 <实参表>与<形参表>中参数的个数、类型和次序应保持一致。 ●当调用无参函数时,函数名后的圆括号不能省略。

1.实参的几种形式 ●形参为简单类型变量,对应的实参可以是:常量,变量及表达式。 ●形参为数组,对应的实参为数组(名)。 ●形参为结构类型,对应的实参为结构类型变量。 如:调用已知三边求三角形面积的函数Area。 double Area(double,double,double); //函数声明 cout<

2.函数调用的形式 (1)函数调用作为一个独立的语句(用于无返回值的函数)调用的形式为: 函数名(实参表);或函数名(); 如:调用print_char函数(用户定义的无返回值函数)。 print_char(‘*’,6); //连续显示6个‘*’字符。

分析函数调用关系图(call graph)的几种方法

分析函数调用关系图(call graph)的几种方法 绘制函数调用关系图对理解大型程序大有帮助。我想大家都有过一边读源码(并在头脑中维护一个调用栈),一边在纸上画函数调用关系,然后整理成图的经历。如果运气好一点,借助调试器的单步跟踪功能和call stack窗口,能节约一些脑力。不过如果要分析的是脚本语言的代码,那多半只好老老实实用第一种方法了。如果在读代码之前,手边就有一份调用图,岂不妙哉?下面举出我知道的几种免费的分析C/C++函数调用关系的工具。 函数调用关系图(call graph)是图(graph),而且是有向图,多半还是无环图(无圈图)——如果代码中没有直接或间接的递归的话。Graphviz是专门绘制有向图和无向图的工具,所以很多call graph分析工具都以它为后端(back end)。那么前端呢?就看各家各显神通了。 调用图的分析分析大致可分为“静态”和“动态”两种,所谓静态分析是指在不运行待分析的程序的前提下进行分析,那么动态分析自然就是记录程序实际运行时的函数调用情况了。 静态分析又有两种方法,一是分析源码,二是分析编译后的目标文件。 分析源码获得的调用图的质量取决于分析工具对编程语言的理解程度,比如能不能找出正确的C++重载函数。Doxygen是源码文档化工具,也能绘制调用图,它似乎是自己分析源码获得函数调用关系的。GNU cflow也是类似的工具,不过它似乎偏重分析流程图(flowchart)。 对编程语言的理解程度最好的当然是编译器了,所以有人想出给编译器打补丁,让它在编译时顺便记录函数调用关系。CodeViz(其灵感来自Martin Devera (Devik) 的工具)就属于此类,它(1.0.9版)给GCC 3.4.1打了个补丁。另外一个工具egypt的思路更巧妙,不用大动干戈地给编译器打补丁,而是让编译器自己dump出调用关系,然后分析分析,交给Graphviz去绘图。不过也有人另起炉灶,自己写个C语言编译器(ncc),专门分析调用图,勇气可嘉。不如要是对C++语言也这么干,成本不免太高了。分析C++的调用图,还是借助编译器比较实在。 分析目标文件听起来挺高深,其实不然,反汇编的工作交给binutils的objdump 去做,只要分析一下反汇编出来的文本文件就行了。下面是Cygwin下objdump -d a.exe的部分结果: 00401050 <_main>: 401050: 55 p

C语言函数调用规定[试题]

C语言函数调用规定[试题] 在C语言中~假设我们有这样的一个函数: int function,int a~int b, 调用时只要用result = function,1~2,这样的方式就可以使用这个函数。但是~当高级语言被编译成计算机可以识别的机器码时~有一个问题就凸现出来:在CPU中~计算机没有办法知道一个函数调用需要多少个、什么样的参数~也没有硬件可以保存这些参数。也就是说~计算机不知道怎么给这个函数传递参数~传递参数的工作必须由函数调用者和函数本身来协调。为此~计算机提供了一种被称为栈的数据结构来支持参数传递。 栈是一种先进后出的数据结构~栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项,被称为栈顶,。用户可以在栈顶上方向栈中加入数据~这个操作被称为压栈,Push,~压栈以后~栈顶自动变成新加入数据项的位置~栈顶指针也随之修改。用户也可以从堆栈中取走栈顶~称为弹出栈,pop,~弹出栈后~栈顶下的一个元素变成栈顶~栈顶指针随之修改。 函数调用时~调用者依次把参数压栈~然后调用函数~函数被调用以后~在堆栈中取得数据~并进行计算。函数计算结束以后~或者调用者、或者函数本身修改堆栈~使堆栈恢复原装。 在参数传递中~有两个很重要的问题必须得到明确说明: 当参数个数多于一个时~按照什么顺序把参数压入堆栈 函数调用后~由谁来把堆栈恢复原装 在高级语言中~通过函数调用约定来说明这两个问题。常见的调用约定有: stdcall cdecl

fastcall thiscall naked call stdcall调用约定 stdcall很多时候被称为pascal调用约定~因为pascal是早期很常见的一种 教学用计算机程序设计语言~其语法严谨~使用的函数调用约定就是stdcall.在Microsoft C++系列的C/C++编译器中~常常用PASCAL宏来声明这个调用约定~类似的宏还有WINAPI和CALLBACK. stdcall调用约定声明的语法为,以前文的那个函数为例,: int __stdcall function,int a~int b, stdcall的调用约定意味着:1,参数从右向左压入堆栈~2,函数自身修改堆栈 3,函数名自动加前导的下划线~后面紧跟一个@符号~其后紧跟着参数的尺寸以上述这个函数为例~参数b首先被压栈~然后是参数a~函数调用 function,1~2,调用处翻译成汇编语言将变成: push 2 第二个参数入栈 push 1 第一个参数入栈 call function 调用参数~注意此时自动把cs:eip入栈 而对于函数自身~则可以翻译为: push ebp 保存ebp寄存器~该寄存器将用来保存堆栈的栈顶指针~可以在函 数退出时恢复 mov ebp~esp 保存堆栈指针 mov eax~[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp~cs:eip~a~b~ebp +8指向a add eax~[ebp + 0CH] 堆栈中ebp + 12处保存了b

Win32环境下函数调用的堆栈之研究

Win32环境下函数调用的堆栈之研究 1.背景说明 由于阅读《Q版缓冲区溢出教程》的需要理解和掌握栈的相关知识,故而使用VC 6.0工具来研究win32环境下函数调用时具体的栈操作。 阅读本文建议先看结论,大概了解相关概念,再看第4节,更易于理解。 2.C原程序 主函数main()和子函数func1都定义了不同类型的局部变量, 并且子函数定义了两个以上(包括两个)的参数. 这样实例源代码可以较充分地说明各种情况。在本实例没有体现的情况, 建议另外添加代码来运行分析。 #include #include void func1(int input1, int input2) { int j; char c; short k; j = 0; c = 'a'; k = 1; printf("sum=%d\n", input1+input2); return; } int main() { char output[8] = "abcdef"; int i, j; i=2; j=3; func1(i,j); printf("%s\r\n", output); return 0; }

3.汇编代码 在VC中,按F10进入DEBUG模式。右键弹出菜单,选择“Go To Disassembly”,则显示C源程序的相应汇编代码。注意:这里的汇编代码是DEBUG模式的,与RELEASE模式的汇编代码会有所不同,但我们将要研究的问题。下面的代码完全摘自VC中,未作任何修改。由此可见,子函数存储区(低址)和主函数存储区(高址)之间还有一些空白区。函数中调用的其他库函数存储在更高的地址,具体情况在实践在查看。 ……(前面省略) --- F:\QBufOverflow\rptBugDlgBox\rptBugDialogBox.c -------------------- 1: #include 2: #include 3: 4: void func1(int input1, int input2) 5: { 00401020 push ebp 00401021 mov ebp,esp 00401023 sub esp,4Ch 00401026 push ebx 00401027 push esi 00401028 push edi 00401029 lea edi,[ebp-4Ch] 0040102C mov ecx,13h 00401031 mov eax,0CCCCCCCCh 00401036 rep stos dword ptr [edi] 6: int j; 7: char c; 8: short k; 9: 10: j = 0; 00401038 mov dword ptr [ebp-4],0 11: c = 'a'; 0040103F mov byte ptr [ebp-8],61h 12: k = 1; 00401043 mov word ptr [ebp-0Ch],offset func1+27h (00401047) 13: 14: printf("sum=%d\n", input1+input2); 00401049 mov eax,dword ptr [ebp+8] 0040104C add eax,dword ptr [ebp+0Ch] 0040104F push eax 00401050 push offset string "sum=%d\n" (0042001c) 00401055 call printf (00401130) 0040105A add esp,8 15: 16: return; 17: } 0040105D pop edi 0040105E pop esi 0040105F pop ebx 00401060 add esp,4Ch 00401063 cmp ebp,esp

函数反汇编以及栈结构分析.doc

#inelude using namespace std; int func(int a 」nt b); void main() { int x = 1; int y = 2; int z = func(x,y); } int func(int a,int b) { return a + b; } 汇编解析: void main() { 〃这个函数头产牛的汇编代码是: main: 013E13A0 push ebp O13E13A1 mov ebp,esp O13E13A3 sub esp,0E4h 013E13A9 push ebx O13E13AA push esi O13E13AB push edi O13E13AC lea edi,[ebp-0E4h] O13E13B2 mov ecx,39h O13E13B7 mov eax,OCCCCCCCC h O13E13BC rep stos dword ptr es:[edi] ******************************************************* int x = 1; int y = 2; 〃这个的汇编代码是: O13E13BE mov O13E13C5 mov ******************************************************* 〃接下来就是函数的调用了,先是把参数压栈 O13E13CC mov O13E13CF push 013E13D0 mov eax,dword ptr [y] eax ecx,dword ptr [x] dword ptr [x],l dword ptr [y],2

C++高效获取函数调用堆栈

C++高效获取函数调用堆栈 问题:在程序的设计开发过程中,往往由于设计上的不足、编程上考虑得不周全或一些失误会导致程序的崩溃,影响了项目的进展,所以程序实现应该是异常安全的。当出现了问题,需要能够快速找到问题所在,并确定出程序的上下文环境。若能重现出现问题时的函数调用堆栈,对解决问题会有很大的帮助。 以往打印函数堆栈一般是使用DbgHelp.dll提供的功能进行。该方法需要额外链接微软提供的库,该库有强大的功能,但使用上也比较复杂。在此就不作介绍,有兴趣可以另外查阅书籍。 这里介绍一下另外一种实现方案,采用程序运行堆栈回溯确定函数的调用地址,并根据VC编译出来的map文件进行定位函数地址。该方法功能单一,使用简单,效率较高。 1、背景知识 首先介绍一下该技术要用到的一些背景知识,一是函数调用堆栈,另一个就是异常处理。 1.1函数调用堆栈 调用堆栈与调用约定关系密切,平常编程中使用的_cdecl、__stdcall、__fastcall、WINAPI、APIENTRY、CALLBACK、PASCAL都是调用约定。分类上有C语言调用约定、Pascal语言调用约定、This调用约定、快速调用约定、裸调用约定 1.1.1 C语言调用约定 参数从右到左入栈,个数可变,调用函数者负责堆栈清理,性能比较低 1.1.2 Pascal语言调用约定 参数从右到左入栈,个数固定,函数体本身就能知道传进来的参数个数 大部分的Windows API都采用Pascal语言调用约定 1.1.3 This调用约定 调用约定跟PASCAL语言调用约定相同,只是另外通过ECX寄存器传送一个额外的参数—this指针

函数调用--函数栈

函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢? 对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写 数据段:保存初始化的全局变量和静态变量,可读可写不可执行 BSS:未初始化的全局变量和静态变量 堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行 栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行 如图所示 寄存器 EAX:累加(Accumulator)寄存器,常用于函数返回值 EBX:基址(Base)寄存器,以它为基址访问内存 ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器 EDX:数据(Data)寄存器,常用于乘除法和I/O指针 ESI:源变址寄存器 DSI:目的变址寄存器 ESP:堆栈(Stack)指针寄存器,指向堆栈顶部 EBP:基址指针寄存器,指向当前堆栈底部 EIP:指令寄存器,指向下一条指令的地址 源代码 int print_out(int begin, int end) { printf("%d ", begin++); int *p; p = (int*)(int(&begin) - 4);

if(begin <= end) *p -= 5; return1; } int add(int a, int b) { return a+b; } int pass(int a, int b, int c) { char buffer[4] = {0}; int sum = 0; int *ret; ret = (int*)(buffer+28); //(*ret) += 0xA; sum = a + b + c; return sum; } int main() { print_out(0, 2); printf("\n"); int a = 1; int b = 2; int c; c = add(a, b); pass(a, b, c); int __sum; __asm { mov __sum, eax } printf("%d\n", __sum); system("pause"); } 函数初始化 28: int main() 29: {

c语言中栈的调用

C语言中,函数调用过程中,栈增长的方向是怎样的?形参和实参是如何关联的? 对于i386系列机器而言,栈是从高地址向低地址增长的。一般函数本身的汇编开始时这样的。 PUSH EBP MOV EBP ,ESP SUB ESP 0C0 这几句可以看出,通过给ESP来减去一个数来打开栈帧,可以看出堆栈是高到低增长的。但是对于别的机器,就不一定是这样。 形参是编译器处理函数调用时的依据,对于i386机器来说,它是先根据形参的类型,在堆栈上压入实参的值,然后调用函数,函数内部使用实参的时候,通过类似EBP-4的方法取实参值。这样函数foo(int a )的调用类似于这样。 PUSH EDX CALL foo 而foo中则通过EBP-4来使用实参。由于函数中打开了栈帧,因此肯定有类似于ADD ESP 0C0的方法来平衡堆栈。如果对于多个参数,压栈顺序则根据编译器设置的调用约定来压入。当然,对于别的CPU,不一定是这样的处理,例如有的会将前面几个参数,固定的通过通用寄存器来传递,其他的参数通过堆栈来传递。 方向是和系统相关的。虽然是编译器决定的,但是属于编译器后端,和硬件相关的部分来实现的。C语言标准中并没有约束,也无法约束,栈的增长方向。 形参,实参。准确说,有两种参数传递方式。 1、寄存器类的间接传递。一个函数中的变量作为子函数的参数传递,该变量被读取到寄存器里给予子函数,寄存器不足时,根据实际系统情况,放在堆栈或其他存储区域,而子函数从寄存器中获取使用,并不会从该变量在父函数的实际存储空间里获取和使用。 2、变量地址的直接传递。本质是将该变量的地址,作为寄存器类的间接方法给予子函数。而子函数获取了这个地址后,直接对父函数变量所存储的空间里的内容进行使用(读取或写出)。 见下图,假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述: (1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。 (2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B 的栈底)。

相关文档
最新文档