首页 > 编程语言 >逆向——C语言的汇编表示之堆栈图 手把手示例 可以考虑在函数内部加一个局部变量来综合理解

逆向——C语言的汇编表示之堆栈图 手把手示例 可以考虑在函数内部加一个局部变量来综合理解

时间:2023-04-02 20:56:40浏览次数:47  
标签:return 函数 示例 int 手把手 C语言 plus 堆栈 ebp

课程概要

来自:https://gh0st.cn/Binary-Learning/C%E8%AF%AD%E8%A8%80.html 写得非常详细

本章课程需要具备汇编语言基础,若无汇编语言基础是无法去理解课程中所讲的一些知识点和技术细节的;同时也表示本课程是以汇编语言来理解C语言,透过本质理解高级语言。

关于本节课的环境:VC6,VC6是一个集成开发环境,使用VC6而不去使用较新的VS是因为VS会自己优化代码,而我们想要直接了解真正的本质就应该选择无添加的VC6。

C语言的汇编表示&函数的定义与调用

在了解C语言的汇编表示之前,我们要弄清楚C、C++、VC6、VS之间的关系,C和C++都属于编程语言,VC6、VS属于集成开发环境。

我们创建第一个C程序的顺序为(以下键盘快捷方式基于VC6):

1.创建项目(选择Win32 Console Application)

images/download/attachments/12714021/image2021-2-5_18-34-38.png

2.创建文件(Source File)

images/download/attachments/12714021/image2021-2-3_19-20-31.png

3.编写入口程序

images/download/attachments/12714021/image2021-2-3_19-39-45.png

34.构建(F7)

images/download/attachments/12714021/image2021-2-3_19-40-15.png

5.运行(F5)

images/download/attachments/12714021/image2021-2-3_19-40-40.png

如下代码就是入口函数:

void main() { return; }

在C语言中约定俗成的入口函数名称为main(),函数的格式是这样的:

返回类型 函数名(参数列表) { 函数体; return 返回类型对应的数据; // 执行结束 }

定义一个函数,其返回类型、函数名是必须要有的,参数列表是可有可无的,定义函数在函数体的最后一定需要使用return返回对应数据类型的数据。

关于函数名、参数名的命名也是有要求的,如下所示:

  1. 只能以字母、数字、下划线组成;且第一个字母必须是字母或下划线。

  2. 命名严格区分大小写

  3. 不能使用C语言的关键字(例如:void、return之类)

定义好函数之后,我们需要知道如何调用函数(使用函数),假设现在我们需要做一个加减法的程序,可以这样写:

int plus(int x, int y) { return x+y; }   void main() { plus(1,2); return; }

如上所示,调用函数的格式为:函数名(传入参数);,这是C语言调用函数的方法,我们之前也了解过汇编如何调用函数

push 0x1 push 0x2 call address

那么C语言其调用函数的本质是什么呢?我们可以来具体看看其编译后的反汇编代码。

单击plus(1,2);那一行,按一下F9,下一个断点,然后F7构建,F5运行。

images/download/attachments/12714021/image2021-2-10_21-36-33.png

再右击这行代码,选择如下图所示的按钮,来查看反汇编代码:

images/download/attachments/12714021/image2021-2-10_21-37-26.png

通过查看反汇编代码我们发现C语言调用函数实际上跟我们之前所学的汇编是一样的:

images/download/attachments/12714021/image2021-2-10_21-41-15.png

但需要注意的是,这里我们看见的反汇编代码是Debug版本,也就是方便我们调试的,而实际上程序编译应该是以Release版本,两个版本对应的汇编代码也是不一样的,另外VC6在展示反汇编代码的时,适当的做了一些优化,也就是便于阅读理解,例如上图所示的函数调用的汇编call指令,实际上就是call 0040100a

总结:函数名本质上就是编译器给内存地址起的名字。

 

以下汇编代码需要熟悉了解(plus函数的汇编代码实现):

1: 2: int plus(int x, int y) { 00401010 push ebp 00401011 mov ebp,esp 00401013 sub esp,40h 00401016 push ebx 00401017 push esi 00401018 push edi 00401019 lea edi,[ebp-40h] 0040101C mov ecx,10h 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi] 3: return x+y; 00401028 mov eax,dword ptr [ebp+8] 0040102B add eax,dword ptr [ebp+0Ch] 4: } 0040102E pop edi 0040102F pop esi 00401030 pop ebx 00401031 mov esp,ebp 00401033 pop ebp 00401034 ret

 

VC6的快捷键:

下断点:F9

运行:F5

构建:F7

编译:Ctrl+F7

构建执行:Ctrl + F5

执行下一条:F10

执行下一条(步入内部):F11

停止调试:Shift + F5

参数传递与返回值

在上一节中我们了解到了函数,函数的本质就是一堆指令,我们可以重复调用;函数的定义在上节中我们也已经了解了,我们举一个函数的例子:

int plus(int x, int y) { return x+y; }

在这个函数中,其参数列表有x和y,它们我们可以理解为是一个占位符,当我们想要调用函数的时候,可以使用真正的数据替换这两个占位符。(:占位符也需要指定其数据大小,也就是数据宽度;不可以直接写作x, y)

该函数plus前面有一个int,这就表示plus函数返回类型int类型,而int类型也是表示数据宽度,其为4个字节,除此之外还有short(2个字节)char(1个字节)

我们想要了解程序的本质,就需要追踪每一行到底是如何运作的,如下代码我们来进行跟踪分析plus函数是如何运行的:

int plus(int x, int y) { return x+y; }   void main() { plus(1,2); return; }

老规矩我们基于VC6的环境下,在调用plus函数那一行下断点(F9),然后(F7)构建,(F5)运行,右键进入汇编界面。

images/download/attachments/12714021/image2021-2-12_23-47-35.png

在这里,我们需要观察堆栈来观察程序的本质,这里可以借助Excel工具堆栈图便于理解,我们可以选中一列然后将其边框都填上:

images/download/attachments/12714021/image2021-2-12_23-51-12.png

记住堆栈在执行前后的变化,画堆栈图要记住两个寄存器,一个是栈顶(ESP),一个是栈底(EBP)。

在我们代码(调用plus)函数还没开始执行时候要先记住这两个寄存器的值:

images/download/attachments/12714021/image2021-2-12_23-54-14.png

将两个值填入我们的Excel表格中,再将其用颜色标记一下即可:

images/download/attachments/12714021/image2021-2-12_23-55-52.png

接下来我们就可以按照程序执行顺序来进行跟进了,我们来看一下汇编代码:

images/download/attachments/12714021/image2021-2-13_0-1-8.png

可以看见从右到左,依次压入我们调用函数传入的参数,然后再使用call指令去调用函数,在这里我们可以使用F10跟进执行

连续压入堆栈2个数据,堆栈也会根据数据宽度提升,此时我们要在堆栈图中根据变化进行修改:

images/download/attachments/12714021/image2021-2-13_0-3-46.png

而我们想要跟进call指令需要使用F11跟进,就如同学习汇编时「使用DTDebug 跟进CALL指令不能使用F8要是用F7」。

而跟进call指令之后,我们的堆栈也会发生变化,call指令下一行执行的地址会压入堆栈,栈顶也随之提升,需要注意的是在VC6中F11跟进会先过渡到一个jmp指令,然后再通过其跳到真正的函数执行地址。

images/download/attachments/12714021/image2021-2-13_0-28-42.png

images/download/attachments/12714021/image2021-2-13_0-48-50.png

images/download/attachments/12714021/image2021-2-13_0-50-49.png

接着我们再来看一下跟进的函数对应的汇编代码:

images/download/attachments/12714021/image2021-2-13_0-52-39.png

在这里看到汇编代码,我们就应该知道它要干什么了,就要通过ebp进行寻址,关于这一块,在学习汇编时也有了解到,所以还是建议各位在学习本课程时候先去学习汇编

我们先来看一下return之前的汇编代码:首先压入ebp到堆栈中,然后提升栈底(ebp)到栈顶(esp)的位置,再将栈顶(esp)提升0x40(十进制则表示64,堆栈图中也就是16个格子,这一块区域我们称之为缓冲区),后将ebx、esi、edi分别压入堆栈(此处是保存现场,为了函数执行完后恢复),而后lea指令是将ebp-0x40的地址(也就是esp提升0x40后的地址)给到edi,再将0x10(十进制则表示16)给到ecx(这里ecx是循环计数器),接着将0xCCCCCCCC给到eax,然后rep stosd(简写)就是将eax的值储存到edi指定的内存地址,默认情况下标志寄存器的 DF位为0,所以edi的值也就随循环每次递增4(dword为4字节所以是4)在这里实际上就是将哪一块缓冲区填充CC,此时堆栈图变成如下所示:

images/download/attachments/12714021/image2021-2-13_1-23-20.png

为什么缓冲区填充的数据是0xCCCCCCCC?因为CC可以起到断点的作用,填充CC就是以防程序使用缓冲区时用过了,如果用过了可以及时断点;这一块包含调试器的一些知识,这里不过多阐述。

return的汇编代码则很简单:通过ebp寻址获得传递的参数,ebp+8则是1,ebp+0xC则是2,最终结果在eax中。

当函数执行完成之后我们需要将之前压入堆栈的寄存器还原,分别pop edi → esi → ebp(堆栈遵循先入后出),而后就是恢复堆栈到函数执行之前的样子,将esp下降到ebp的位置,而后再pop ebp,还原栈底,最后ret也就是将当前栈顶的值赋给eip,然后让栈顶加4(注:这里之前使用过的数据都不会清空,如果程序运行时敏感数据存储在堆栈内则会被黑客恶意利用),但此时结束了吗?并没有,我们F10继续跟进:

images/download/attachments/12714021/image2021-2-13_1-37-20.png

可以清晰的看见esp的值加0x8,此时才是遵循了堆栈平衡,还原了堆栈在函数执行前的样子。

images/download/attachments/12714021/image2021-2-13_1-38-42.png

最后,我们来总结一下:

  1. 在C语言中参数传递是通过堆栈的,传递的顺序是从右到左

  2. 在C语言中函数返回值是存储在寄存器eax中

 

变量

在编写程序的时候,经常需要存储数据,前面学习汇编时了解到了,数据可以存在寄存器中,或者内存中。在C语言中,存储数据要存在变量中,变量就是一个容器(通常就是内存);变量类型决定变量内存宽度,变量名就是内存地址的编号(别名)。

声明变量的格式:

变量类型 变量名;

注:变量类型用来说明数据宽度是多大

例:int 4字节、short 2字节、char 1字节

变量的命名规范与函数名、参数名一样:

  1. 只能以字母、数字、下划线组成;且第一个字母必须是字母或下划线。

  2. 命名严格区分大小写

  3. 不能使用C语言的关键字(例如:void、return之类)

在C语言中,变量有两类:全局变量、局部变量。

全局变量

  1. 在函数体外定义,并且作用于全局;

  2. 在程序编译完成后,内存地址和宽度就已经确定下来了,变量名就是内存地址的别名;

  3. 只要程序启动,全局变量就已经存在,如若变量在一开始声明时没有赋值,则初始值为0;

  4. 如果不重新编译,全局变量的内存地址永远都不会变;

  5. 全局变量中的值任何程序都可以改,其最终存储的就是最后一次修改的值。

在这里我们简单的来定义一个全局变量:

int a = 123;   int plus() { return a+1; }   void main() { plus(); return; }

我们跟进看一下反汇编代码:

images/download/attachments/12714021/image2021-2-13_2-5-44.png

全局变量a实际上就是一个内存地址,接着来看其存储的内容(0x7B转为十进制就是123):

images/download/attachments/12714021/image2021-2-13_2-6-51.png

局部变量

  1. 在函数体内定义,作用于当前所在函数;

  2. 局部变量的内存是在堆栈中分配的,程序执行时才分配,不执行则不会分配,我们无法预知程序何时执行,也就意味着我们无法知道局部变量的内存地址;

  3. 不确定局部变量的内存地址,所以其也就只能作用于当前函数内部,其他函数不能使用。

int plus() { int a = 123; return a+1; }   void main() { plus(); return; }

最后结论:

  1. 全局变量是可以没有初始值直接使用的,因为系统默认给其0为初始值;

  2. 局部变量在使用前必须要赋值,因为系统不会初始化它,而只有在其赋值时才会分配内存。

变量与参数的内存布局

我们已经掌握了函数、函数调用、变量、参数、返回值等相关的一些概念,本借口我们从内存的角度来分析参数在内存中的位置局部变量在内存中的位置返回值是如何返回和使用的

以下示例代码,就是一个调用函数(x+y),结果给了局部变量z,然后返回z:

int plus(int x, int y) { int z = x + y; return z; }   void main() { plus(1,2); return; }

在这里,我们还是下断点一步一步跟进,然后画堆栈图分析即可,如下是堆栈图及其对应汇编代码(这不是整个函数执行完后的堆栈图):

images/download/attachments/12714021/image2021-2-13_14-50-22.png

如上图所示,我们可以清晰的看见参数在内存中的位置就是ebp+8、ebp+c...以此类推;局部变量则位于我们之前所说的缓冲区,也就是ebp-4、ebp-8...以此类推,这也就是为什么局部变量使用前需赋初值,不然里面是垃圾数据(CC)。

接下来我们需要知道返回值是如何返回和使用的,在C语言中使用返回值就需要一个容器来存储这个返回值,这个容器我们也称之为变量,如下示例代码:

int plus(int x, int y) { int z = x + y; return z; }   void main() { int a; a = plus(1,2); return; }

我们来看下汇编代码:

images/download/attachments/12714021/image2021-2-13_15-0-24.png

可以看见这里会将eax放入到当前函数的缓冲区(main函数,ebp-4),也就是将返回值存到当前函数的缓冲区内

函数嵌套调用的内存布局

函数嵌套调用实际上就是函数中调用另外一个函数,以下为示例代码:

int plus1(int x, int y) { return x+y; }   int plus(int x, int y, int z) { int r; r = plus1(x,y); return r+z; }   void main() { int a; a = plus(1,2,3); return; }

老规矩我们还是在plus函数那下断点跟踪画堆栈图,如下是堆栈图:

images/download/attachments/12714021/image2021-2-13_15-24-35.png

在调用完plus1函数后,plus函数会有这样一个汇编代码:

00401094 add esp,44h 00401097 cmp ebp,esp 00401099 call __chkesp (004010b0)

需要注意的是这个代码只有Debug版本才会有,而在Release版本中堆栈的布局与这是不一样的。

这段代码的意思就是对比esp和ebp是否一样,而我们知道堆栈在使用完成之后要恢复成员来的样子(堆栈平衡),所以在add指令之后ebp与esp应该是一样的,而后的call指令实际上就是调用了一个函数(__chkesp),这个函数就是用来检查你的堆栈是否平衡的。

至此,我们就了解了函数嵌套调用的内存布局,但实际上我们在之前就已经了解过了,因为main本身也是一个函数,main调用了plus也属于函数嵌套调用,只不过我们画堆栈图是在调用plus之前画的,所以忽略了这一点。

 

 

标签:return,函数,示例,int,手把手,C语言,plus,堆栈,ebp
From: https://www.cnblogs.com/bonelee/p/17281327.html

相关文章

  • C语言逆向分析——IF条件语句的反汇编,要熟悉各种if的姿势以及与或非表达式组合
    第四课IF语句的反汇编判断  第四课练习1intplus(intY,intX){intN=t;if(X<Y)t=t+Y;//t=N+yC}voidmain(){plus(5,4);}第五课IF...ELSE...语句的反汇编判断IF…ELSE…语句的反汇编判断:  ......
  • [每天例题]蓝桥杯 C语言 单词分析
    蓝桥杯C语言单词分析题目  题目要求1.寻找出现最多的字母和这个字母出现的次数。2.如果有多个字母出现的次数相等,输出字典序最小的那个。思路分析输入方法:方法一:1.可以通过数组来记录该单词,并为单词出现的每一个字母做上标记。2.可以采用for循环将字符串依次输......
  • 使用 SK 示例 探索 GitHub 存储库 中的机器人
    微软3月22日一篇文章“Semantic-kernel嵌入和记忆:使用聊天UI探索GitHubRepos”[1],文章中进行了展示了嵌入,该文章解释了他们如何帮助开发人员提出有关GitHub存储库的问题或使用自然语言查询探索GitHub存储库。与嵌入一起,这是在SK存储器[2](嵌入集合)的帮助下完成的,这有助于为提......
  • C语言学习记录(终)
    C语言学习记录(终)一、知识要点(文件)一、文件和文件类型指针1、流和文件的概念流的概念我们不管什么物理设备硬件什么的,就把数据的输入输出抽象化为一种流,这样就可以方便编程,具有通用性。流分为文本流和二进制流文本流中的数据以字符形式存放,每个字符用ASCII码表示,占一个字......
  • C语言逆向——如何寻找main入口,一个反汇编成C的实战练习
    第二节2.3找程序的入口原文:https://www.showdoc.com.cn/fengxin1225/7054696489361869控制台应用程序的main函数入口在OD中找到以上其他中的函数,然后跟着3个参数的CALL就是main例:———————————————————- 第二节2.4.1逆向并还原为C代码(网上解答)......
  • 跟着鹏哥学C语言-第四天
    if语句if(表达式)语句; if(表达式)语句1;else语句2;多分枝if(表达式1)语句1;elseif(表达式2)语句2;else语句3;如果条件成立,要执行多条语句,则应该使用代码块intmain(){ intage=40; if(age<18) printf("未成年\n"); elseif(age>=18&&age<=28) printf("青年人......
  • PAT Basic 1059. C语言竞赛
    PATBasic1059.C语言竞赛1.题目描述:C语言竞赛是浙江大学计算机学院主持的一个欢乐的竞赛。既然竞赛主旨是为了好玩,颁奖规则也就制定得很滑稽:0、冠军将赢得一份“神秘大奖”(比如很巨大的一本学生研究论文集……)。1、排名为素数的学生将赢得最好的奖品——小黄人玩偶!2、......
  • C语言逆向汇编——参数局部变量、函数堆栈、调用约定和内嵌汇编码
    第一节1.1C语言1、注意参数和局部变量在堆栈中的存储方式2、参数在调用函数前就已经存入堆栈,从[EBP+8]、[EBP+C]、……开始。3、局部变量是在调用函数后,存入缓冲区里,从[EBP-4]、[EBP-8]、……开始4、函数运算得到的结果,通常存在EAX里。        第一......
  • GitHub OAuth 第三方登录示例
     ⇐  ⇒GitHubOAuth第三方登录示例教程作者:阮一峰日期:2019年4月21日这组OAuth系列教程,第一篇介绍了基本概念,第二篇介绍了获取令牌的四种方式,今天演示一个实例,如何通过OAuth获取API数据。很多网站登录时,允许使用第三方网站的身份,这称为"第三......
  • c语言include文件路径(c语言include相对路径)
    原文:http://www.easyaq.com/post/11694.html今天给各位分享c语言include文件路径的知识,其中也会对c语言include相对路径进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!本文目录一览:1、C语言include能不能是相对路径?2、C语言中,include是什么意思?3、#incl......