第一章

全局变量引发的故事

1 程序存储区

程序存储区一般有下列几段:
程序代码区(SECTION.txt ):
用来存放可执行文件的操作指令(二进制),也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。在text段中,也有可能包含一些只读的常数变量。
全局区:
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(SECTION.data),未初始化的全局变量和未初始化的静态变量在相邻的另—块区域(bss segment),程序载入时由内核清零,是静态内存分配。程序结束后由系统释放。

上为已赋值区,下为未赋值区。
但这个分区只在以前C语言中使用,现在的C++已经不再区分这两个区。

文字常量区(SECTION.rdata):
常量字符串就是放在这里的,程序结束后由系统释放,主要存放const声明的内容。


该区为只读区,必须赋初值,且不可修改。

注意:
上面几个区都是在程序执行前就被加载好到内存中的,这些数据段中的数据在程序根据IMAGE_BASE加载到内存指定位置后,会经过重定位直接加载到内存中指定位置。(这一步骤就是我们常说的全局变量在程序开始执行的时候就分配了空间,并在程序结束时释放空间)
而堆区和栈区是在程序执行时动态变化的。

堆区(heap):

存放程序员自己申请释放的内容,若程序员不释放,程序结束后会由操作系统自动释放。分配方式类似链表。

注意:虽然int(3)和sizeof(int)的4*4=16个字节数据是在堆中,但i和a这些局部变量的8字节还是在栈中的。即堆里存的是数据,栈里存在的是指向这写数据开头的地址(指针)。
堆里数据如果不释放,程序结束前会一直存在(即内存泄漏)。

栈区(stack):
由编译器自动分配释放,存放函数的参数,局部变量,返回地址等等,栈里数据在对应函数结束后会由系统自动释放。

2 C语言函数调用入栈顺序:

ESP指向栈顶是低地址,EBP指向栈顶是高地址。
EIP:指向当前栈帧中执行的指令(即指向下条命令的地址,代码区的地址)
每次压栈,都执行
push 数值
这一汇编代码,这条命令的作用是将数值压入栈顶,同时ESP值+4,因为32位机上压入栈的数值均用4字节为单位保存,即最小也要占用四字节空间。

先参数:从右到左;后返回地址;最后函数内声明的局部变量
局部变量按顺序由高地址到低地址存放,但对于某个局部变量,其数据存放是从低地址到高地址存,例:EB09 ,先存09,再存EB,由下到上,由低到高,因此可以利用溢出攻击覆盖在其上方的返回地址。
数组的话,下标大的先入栈在高地址,下标小的后入栈在低地址。

实例1:在main函数里调用一个函数print_out,栈变化如下:

现在我们来分析这个入栈的过程和顺序:
main函数也是被其他某函数调用的,这里我们就不追究了,因为栈是往低地址增长的,我们可以看出main函数执行过程(即还现在是当前活动函数))是先把main内定义的局部变量入栈了,紧接着是那3个寄存器的内容,此时继续往下执行,发现遇到函数prin_out调用了,这时首先会在栈内开辟两个4字节的空间(因为只发现两个int型的形参),也就是C语言中的声明了两个变量,同时把这两个空间中分别填入0和2,即完成了函数形参的声明和初始化(因为现在还处在main函数栈帧内,所以我们可以看出被调用函数的形参的声明和赋值都是在调用函数中完成的,而不是被调函数自己分配的空间),也就是上面看到的实参1,2在栈中存在了,接下来即将进入print_out函数之前,main函数还得把print_out函数的下一条指令地址(也就是上图的返回地址)给入栈保存起来(这个过程是call print_out汇编指令就会自动完成的,其实这个下一条指令的地址就是回收刚刚分配的那两个实参占用空间的操作,即add esp,8这句指令的地址,不急,我后面会详细分析为什么是这条),因为print_out函数运行完后,此时main函数才知道应该继续怎样运行。(疑问点这个print_out函数的下一条指令的地址不能是print_out函数执行快执行完时自己告诉main函数吗,当然不行,因为print_out函数自己根本不知道自己下一条指令是谁,自己都可能被不同函数调用呢,对外层函数(调用者)一点也不知情)。当把返回地址也压入栈后,就可以进入print_out函数了。

实例2:main函数调用add(1,2)
汇编码:

call指令:

返回地址入栈后,跳入被调用函数,以下操作由被调用函数完成。
将当前函数(main)的ebp(红色箭头处)的值压入栈已保存ebp(上级函数栈底)的值。
此时再将ebp=esp,ebp成为新函数栈底,保持不变。之后栈顶esp自由增长,使用新函数的参数时,用ebp+8,ebp+12等调用。
即每个函数开头汇编代码必定如下:

栈结构如下 main调用add(1,2):

每个函数结束汇编代码必定如下:

先弹出三个,然后esp变为原来(main函数)的esp,然后ebp变为原来(main函数)的ebp,继而ret此时栈里的返回地址(call指令下一条指令地址,4139ab)。

ret指令相当于:
pop eip
jmp eip保存的地址

此时栈里还有1,2。
之后main函数(调用函数)执行add esp,8 (图在上方),相当于忽略掉栈里的1,2。栈状态回到调用add之前,此时eip指向add(1,2),的下一条指令。

3 内存泄漏

指针连续指向内存导致内存无法释放
int *p = new int; 系统给p分配内存
p = new int; 系统继续给p分配内存
此时,原来的内存没有使用,也没有赋给常量,导致一直存在无法释放,称为内存泄漏。
避免:
在第二次new int前,delete(p),释放内存;

理解指针和指针强制转换

1 类型转化和指针转换

长类型转变为短类型是安全的,反之危险,因为长变短相当于损失部分信息,而短变长相当于要多占用其余信息,而被占用的信息可能十分重要导致程序崩溃。

长变短的过程:
int a;
char c=(char)a;

类型转换:int 99
内存中2进制 0110 0011 0000 0000 0000 0000 0000 0000
16进制: 63 00 00 00
只保留最低位的一个字节:63
对应char类型的 ‘c’
由此int转换为char

效果如下:

指针转换同理:

2 指针原理

指针的长度是固定的,sizeof(int*)和sizeof(float*)都是8,因为指针存放的是地址,windows当前内存普遍为64位,8个字节。

int* pi=12
对应汇编代码:

在第⒉条赋值语句中,除了要赋的值(0ch)、被赋值的地址(在eax中)外,还有一个符号我们之前未注意:dword。是它回答了我们的问题:“写几字节?”dword表明写4字节。到此我们发现,其实指针的类型信息决定了赋值/读取时写/读多少字节
读/写多少字节的信息不是存放在指针变量中,而是放到了与该地址相关的赋值指全中.mov指全中的dword指明了这个信息。
C语言之所以要包装出指针的概念,是在汇编地址的内涵上增加了另一层含义,即读/写多少字节。不同类型指针,访问字节数不同。int* 访问4字节,short* 访问2字节。这样就方便我们操控一个地址,否则如果只有地址信息,每次访问它还要附加说明访问的字节数。这时,我们也能理解指针加/减1不是加/减1字节,而是加/减长度为该指针指向类型的长度的字节数。比如,int指针加1是加4字节,short指针则是加2字节。我们也能理解,void *类型的指针为什么无法进行加、减运算。因为它只是汇编语言中的地址,没有类型信息,加、减的时候不知道加/减多少字节。

函数调用和局部变量

1 函数如何获得形参的值

调用函数时,先压入函数的参数,然后压入返回地址,之后跳转到函数中,前面已经提到过,每个函数开始必定执行下列汇编代码:

执行改代码:esp的值距离第一个压入栈的参数(也是最右边的参数)差8个字节,因为参数到esp之间有返回地址的值和ebp的值。
之后把现在的esp值传给ebp,栈顶此时就是原来esp的值,以ebp的值为分界线。之后需要用到参数值时,只需用栈底指针ebp+8获得最右边的形参地址,+12获得第二个,以此类推。

同理,局部变量的寻址也是通过ebp-偏移量来的。

2 函数返回后操作

前面已经提过每个函数结束前必定执行如下汇编代码:

执行后esp的值变为ebp即上面说的原函数esp的值,此时esp指向原函数ebp的值,因此之后pop让ebp获得自己原来未进行函数调用时候的值,esp+4,此时esp指向返回地址的值。
ret操作等同于:
pop eax
jmp eax
让eip得到下条指令的地址(下图的004139ab,是在代码区),使得程序可以根据EIP执行函数调用后的下条指令。(ret和call指令中的jmp都大幅度改变了eip,正常执行下eip应当随着语句小幅度增加,jmp指令相当于直接改写eip)
又让esp+4,此时esp指向最右边形参的值,并跳转到原函数:

程序会根据形参的个数和大小执行一条:
add esp ,偏移量(图中是8)
的命令,让esp的值等于执行调用函数前的状态。

3 函数指针

对于函数:
int add(int a,int b);
其对应指针类型为:
int (*pfuc)(int,int);
这样,pfuc就可以指向add:
pfuc=add; 或 pfuc=&add ; ,pfuc指向add函数的入口地址。
之后可以用pfuc来访问add:
int sum=pfuc(1,2);

数组,结构体

1 C语言数组为什么要以0为第一个元素编号

是因为方便每个数组元素的寻址:

每个元素的寻址都是通过首地址+偏移量来计算的。

无法沟通:对齐的错误

1 结构体里的数据对齐

对于下方代码:

理应的输出长度:1+2=3;但实际输出为4,因为c的长度应为要和a对齐变成了2。
原因:

因此,为了让结构体里所有元素都能遵从上述规律,一般给每个元素都分配最大元素所占的内存量。

switch语句的思考

关于其他高级语言要素的反汇编学习

全局变量的疑问:重定位和程序结构

1 同时执行两个某个程序,其全局变量会共享吗

不会。
在CPU保护模式下,每个执行进程(程序的一个实例)都拥有自己独立的线性地址空间,这种机制叫虚存系统。
程序访问内存用的是线性地址(mov指令中包含的地址信息是线性地址,用户态程序无法直接访问物理内存),而虚存管理系统用该地址找到对应的物理地址进行访问。表面上,两个进程的线性地址是一个地址(就如两个寄存处同一编号的号牌),其实对应的物理内存是不同的,并不会冲突。

2 全局变量地址获取方法

假想程序就是一块数据,指令的机器码、全局变量都在这块数据中,我们可称存放代码的区域叫代码段,存放全局变量的区域叫数据段。执行程序就是这样的数据块。如果将它读出并放入内存,再将EIP寄存器指向main()函数的入口,那么程序就被加载且从main()开始运行。

图1.69(a)表示从右边硬盘将执行程序完全读入左边的内存,然后EEP寄存器中的值指向main()入口。完成加载程序并将EIP寄存器中的值指向main()入口的程序称为加载器。那么,加载器如何知道main()的入口﹖可以构想,执行程序就是一个表,这个表有很多表格,其中某个表格给出了main()的位置。加载器读取表格,获取这些信息即可。那么,main()的入口位置怎样表示?是加载后main()的线性地址还是其他什么﹖其实,main()放在程序的某个位置可以用它相对程序头部的偏移量表示,这样不管程序加载在哪个地址,用偏移量+程序头部地址都能够计算出 main()的地址,这样更灵活,见图1.69(b)。

因为整个程序都是编译器生成的,那么它在编译期就能确定所有的全局变量相对头部的偏移量,只要程序加载到编译器希望加载的地址,则所有全局变量地址在编译期都可计算出:

全局变量地址=程序头部加载地址b + 全局变量相对程序头部的偏移量a

因此,在程序加载地址问题上,为了编译器能计算出全局变量地址,我们应该采取由程序指定它希望被加载的地址,而非加载器随机加载。为了告诉加载器希望加载的地址,我们将执行程序中分配一-个区域来存储这个希望加载的地址,称为image base(基址)。图1.69(b)。

3 动态链接库

先介绍动态链接库的工作原理和编程接口。可如下简单理解动态链接库。首先,结构上,它与EXE文件一样是一个模块,包含代码段,数据段等,甚至有入口点,只不过这个入口点是当DLL被加载时执行一次,而非执行程序那样,作为整个程序的起始点。
其次,DLL 的目的是将一些程序的功能块从物理上分离出来,需要时由程序加载到内存。它对外暴露了可调用的函数,加载程序能获取这些函数地址,从而调用它们。这样做的好处有以下几点:

①如果代码可复用,那么只需要一份文件副本,节省空间。
②形成一定程度的抽象和隔离,用户只是通过函数名获取函数指针,并不知道函数的实现。那么,我们可通过替换新DLL以提供新实现,从而在不影响程序其他部分的情况下修改程序实现。插件的机制就要依赖动态加载。
③能将大项目分解成彼此独立的DLL,便于任务分配,并行开发调试。

Windows下动态链接库相关有3个重要的API:
LoadLibrary,将DLL从硬盘加载到内存;
GetProcAddress,接收函数名作为输入,返回该函数入口地址,当获得函数地址后,即可调用它;
FreeLibrary,卸载已加载库。

老码识途学习笔记(一)相关推荐

  1. 老码识途读书笔记 1

    知识点记录: 1.int 或指针类型的全局变量默认初始化为0,局部变量则为0xcccccccc.(win7 + vs2008 ) 2.内存溢出攻击即使用6个字节空间改变程序执行流程达到某种目的.话说当 ...

  2. 《老码识途》读书笔记:第一章(上)

    <老码识途>读书笔记:第一章--欲向码途问大道,锵锵bit是吾刀(上)   1.赋值语句 对于全局变量赋值语句,例如下面这句: 1 int gi; 2 void main(int argc ...

  3. 老码识途:从机器码到框架的系统观逆向修炼之路 pdf电子书

    重要提示尊敬的用户您好,由于老码识途:从机器码到框架的系统观逆向修炼之路pdf书受百度网盘影响无法做公共分享,只能私密分享,有不到之处请多多谅解! 百度网盘链接: http://pan.baidu.c ...

  4. 《老码识途:从机器码到框架的系统观逆向修炼之路》- 第1章 - 总结

    本章学到了什么 调试技巧:在VS中断点调试,查看反汇编代码,step into进行步进调试,运行过程中查看寄存器.内存地址.变量值变化等. 机器码构造能力:使用C/C++中的直接在C代码里写汇编语言的 ...

  5. 老码识途之对象函数调用

    上一期,我们讨论了普通函数的调用过程,如果没弄明白,看这里 今天所要讲的将是对象调用函数. class C{public:int a;int b;int c;void f(int t){a = t;} ...

  6. 老码识途——在堆中构建mov和jmp指令

    // asmjmp.cpp : 定义控制台应用程序的入口点. // #include <stdio.h> #include <malloc.h>int gi; void * a ...

  7. 老码识途之构造函数和析构函数

    对象初始化过程就是先父类构造函数,再子类构造函数.,那么我们从汇编角度去探索这个过程是怎么样的 class P{public:int a ;P(){a = 1;}~P(){a = 4;} };clas ...

  8. 读书 --- 老码识途

    上周在图书馆借了这本书,这个周末细看了下,是本好书.作者应该是个大学教授叫韩宏.书中讲的很底层,一开始就告诉大家如何debug一段程序,在VS2008里面查看内存.寄存器.反汇编.通过这些来认识汇编. ...

  9. 老码识途1之函数调用和局部变量

    无论在编程中,还是在面试中,都会遇见调用函数这个东东,但是,要是让你说函数是怎么调用的,你能回答上来吗,接下来就让我们一起探索函数如何在汇编层次上实现调用的 在接下来,我们将有几个问题要去解决 函数调 ...

最新文章

  1. 【Python】学习笔记一:Hello world
  2. 开辟经济发展的第二战场
  3. Android之最简单和靠谱的监听Home键和菜单键(最近任务栏)
  4. NeurIPS 2020 所有RL papers全扫荡
  5. hive分桶表join_Hive知识梳理
  6. 【安卓笔记】是否执行测试服务
  7. DevOps使用教程 华为云(9)代码检查
  8. python批量打印mathcad_全能批量打印工具-兼容所有打印机
  9. java字节流字符流复制文件大小不一致及乱码
  10. Arduino 开发 — Arduino 函数库
  11. 嵌入式-stm32学习:使用固件库点亮LED
  12. Sims 4 Cottage Living 模拟人生4乡间生活 作弊码整理
  13. 稀疏编码中的正交匹配追踪(OMP)与代码
  14. 怎样在word中打印框选对√
  15. 【python数模小作业】动手‘预习‘高数之 人口预测(线性拟合)
  16. 【研报】医美行业产业投资宝典:颜值新经济,美丽无止境——附下载链接
  17. CANoe操作介绍系列 ———— Analysi功能区中Graphic的介绍与使用
  18. 分布式事务之——基于消息中间件实现
  19. 如何防止Excel工作表名称被修改
  20. SpringBoot +WebSocket实现简单聊天室功能实例

热门文章

  1. 本题要求实现一个函数,判断任一给定整数N是否满足条件:它是完全平方数,又至少有两位数字相同,如144、676等。
  2. oracle tsm rman,TSM对Oracle数据库备份脚本
  3. 推荐一款可以设计衣服的软件?零基础小白不可错过的服装设计工具
  4. 快速发展的低压电力线载波技术及其应用展望
  5. 基于gdal的空间缓冲区分析(python)
  6. UG10.0压铸模具实战案例设计视频教程-产品分析 流道渣包设计教程
  7. 我正在建造一座大教堂
  8. windows xp主题不见了
  9. 如何真正认识 Linux 系统结构?这篇文章告诉你
  10. PS里面获取像素坐标的方法