在给出的关于虚拟内存的陈述中,简述虚拟内存
本文在一个简单的汇编中理解了目录虚存的布局,进一步理解了堆栈调用和ret其他变量引用。
虚拟内存布局关于虚拟内存中进程的布局,一个经典的解释图是:
在一个完整的汇编程序中,首先要注意的其实是图中的堆栈部分,这是一个地址向低位增长的堆栈。
了解一个简单的汇编如果你想分析汇编程序,一个非常有用的网站是https://godbolt.org/,它可以很容易地把程序翻译成汇编。
网站上提供的例子有:
//在此处键入您的代码,或者加载一个示例。
整数平方(整数){
返回num * num
} x86-64 gcc 11.2编译为
square(int):
推送rbp
mov rbp,rsp
mov DWORD PTR [rbp-4],电子数据交换
移动eax,DWORD PTR [rbp-4]
imul eax,eax
pop rbp
浸水使柔软
背景知识:
Push代表将操作数推入堆栈(指内存中的堆栈)MOV目的地,源。mov指令的效果相当于C /Java中的赋值语句(从右值到左值)destination=sourceImul代表有符号整数乘法,有符号整数乘法pop对应push。
仅仅知道这些指令是什么是不够的。字符rbp、rsp和DWORD PTR都有自己固定的含义。只有了解它们,我们才能理解大会在做什么。
为了比较,我们先来看另一个简单的程序,把它翻译成编译。
#包含stdio.h
int main(void)
{
int a;
a=972
printf(a=%d\n ,a);
return(0);
}对应的程序集是:
00000000040052d main:
40052d: 55推rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83 ec 10 sub rsp,0x10
400535:C7 45 fc cc 03 00 00 mov DWORD PTR[RBP-0x 4],0x3cc
40053c: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
40053f: 89 c6 mov esi,eax
400541: bf e4 05 40 00 mov edi,0x4005e4
400546: b8 00 00 00 00 mov eax,0x0
40054b: e8 c0 fe ff ff呼叫400410 printf@plt
400550: b8 00 00 00 00 mov eax,0x0
400555: c9离开
400556: c3 ret
400557: 66 0f 1f 84 00 00 00 nop字PTR [rax rax*10x0]
40055e: 00 00对比这两个汇编,我们发现有些东西是不变的,这就告诉我们这些东西很重要,需要理解。
看前面几个字。
00000000040052d main:
40052d: 55推rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83EC10SUrsp,0x10函数第一行main指rbp和rbp这些是专用寄存器。Rbp是基址指针,指向当前堆栈帧的基址点,rsp是堆栈指针,指向当前堆栈帧的顶部。
寄存器基址指针.它的作用是校准一个基址,在操作过程中它的值变化很小。
rsp:寄存器堆栈指针.它的作用是校准栈顶,它的值会不断变化。因为堆栈在虚拟内存中的地址是向下增长的,所以堆栈中存储的值看起来会越来越小。
虚拟内存中堆栈部分的初始状态:
图中的栈上以前的值:也就是本文开头图中的命令行参数和env var。注意,这些东西实际上是栈的内容,而不是栈外的内容,所以叫做‘前值’
push rbp指令将寄存器的值rbp压入堆栈。因为它被“推”到堆栈上,所以当前值rsp是新堆栈顶部的内存地址。和堆栈寄存器如上图所示。
Mov rbp,rsp将堆栈指针rsp的值复制到基址指针rbp。Rsp现在指向堆栈的顶部。
SURSP,0x10创建一个空间来存储局部变量的值。rbp和rsp之间的空间就是这个空间。请注意,这个空间足够存储我们的类型变量integer。
sub:减去。SURSP,0x10相当于C \Java中的RSP=RSP-16;
或者因为虚拟内存中的堆栈增长到低位地址位,所以堆栈的顶部滑动到低位地址位。
我们只是在内存中——,在堆栈中3354,为我们的局部变量创建一个空间。这个空间叫做栈帧。每个带有局部变量的函数都将使用堆栈帧来存储这些变量。
我们的函数汇编代码的第四行如下:
400535:C7 45 fc cc 03 00 00 mov DWORD PTR[RBP-0x 4],0x3cc
字是16位,DWORD是双字,32位。这正好是现代c中有符号int的长度,PTR是指针,代表地址。
如前所述,mov相当于C \Java中的赋值,所以这里是一个赋值操作
这一行对应于我们的C代码行:
a=972Movordptr [RBP-0x4],0x3cc将地址rbp-4的存储器设置为972。[rbp-4]是我们的局部变量a,计算机实际上并不知道我们在代码中使用的变量的名字,它只是引用了堆栈上的内存地址。
这是此操作后堆栈和寄存器的状态:
现在当我们看函数的结尾时,我们会发现:
400555: c9 leave该指令leave分为两步:将rsp设置为rbp,然后将栈顶弹出到rbp。
因为我们rbp在进入函数时把前一个值压入堆栈,所以现在rbp被设置为前一个值rbp。
在我们离开当前函数之前,局部变量被“释放”,前一个函数的堆栈帧被恢复。并且堆栈寄存器rbp的rsp状态被恢复到与我们进入主函数时相同的状态。
对堆栈有更深入的了解当变量自动从堆栈中释放出来时,它们并没有被完全“销毁”。它们的值仍然在内存中,可能会被其他函数使用。
这就是为什么写代码的时候初始化变量很重要,就像有效C中的04条款说的:使用对象之前要确保对象已经初始化。否则,当程序运行时,它们将获得堆栈上的任何值。
考虑以下代码:
#包含stdio.h
无效函数1(无效)
{
int a;
int b;
int c;
a=98
b=972
c=a b;
printf(a=%d,b=%d,c=%d\n ,a,b,c);
}
无效函数2(无效)
{
int a;
int b;
int c;
printf(a=%d,b=%d,c=%d\n ,a,b,c);
}
int main(void)
{
func 1();
func 2();
return(0);
}输出
a=98,b=972,c=1070
A=98,b=972,c=1070同一个变量值func1!这是因为堆栈的工作方式。这两个函数以相同的顺序声明相同数量和类型的变量。它们的堆栈帧是相同的。func1结束时,其局部变量值所在的内存不会被清除——只有rsp会增加。
所以当我们调用func2它的栈帧时,它和前面的栈帧func1有着完全相同的位置,局部变量的func2值和我们离开时的func1是一样的。
注意:一个函数对应一个堆栈帧。
对应的编译是:
00000000040052d func1:
40052d: 55推rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83 ec 10 sub rsp,0x10
400535:C7 45 F4 62 00 00 00 mov DWORD PTR[RBP-0xc],0x62
40053c: c7 45 f8 cc 03 00 00 mov双字PTR [rbp-0x8],0x3cc
400543: 8b 45 f8 mov eax,双字PTR [rbp-0x8]
400546: 8b 55 f4 mov edx,双字PTR [rbp-0xc]
400549: 01 d0添加eax,edx
40054 b:89 45 fc mov DWORD PTR[RBP-0x 4],eax
40054e: 8b 4d fc mov ecx,DWORD PTR [rbp-0x4]
400551: 8b 55 f8 mov edx,双字PTR [rbp-0x8]
400554: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
400557: 89 c6 mov esi,eax
400559: bf 34 06 40 00 mov edi,0x400634
40055e: b8 00 00 00 00 mov eax,0x0
400563: e8 a8 fe ff ff电话400410 printf@plt
400568: c9离开
400569: c3 ret
00000000040056a func2:
40056a: 55推rbp
40056b: 48 89 e5 mov rbp,rsp
40056e: 48 83 ec 10 sub rsp,0x10
400572: 8b 4d fc mov ecx,DWORD PTR [rbp-0x4]
400575: 8b 55 f8 mov edx,双字PTR [rbp-0x8]
400578: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
40057b: 89 c6 mov esi,eax
40057d: bf 34 06 40 00 mov edi,0x400634
400582: b8 00 00 00 00 mov eax,0x0
400587: e8 84 fe ff ff电话400410 printf@plt
40058c: c9离开
40058d: c3 ret
00000000040058e主要:
推动rbp
40058f: 48 89 e5 mov rbp,rsp
400592: e8 96 ff ff ff调用40052d func1
400597: e8 ce ff ff ff调用40056a func2
40059c: b8 00 00 00 00 mov eax,0x0
4005a1: 5d弹出rbp
4005a2: c3 ret
4005 a3:66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax rax * 10x 0]
4005aa: 00 00 0
405ad: 0F1F00NOP DWORD PTR [RAX]你可以看到,栈帧总是以同样的方式形成的。在我们的两个函数中,堆栈帧大小是相同的,因为局部变量是相同的。
推送rbp
mov rbp,rsp
SURSP,0x 10两个函数都以语句结束。
变量a、b和C在两个函数中的引用方式相同:
a位于存储器地址rbp-0xc。
b位于存储器地址rbp-0x8。
c位于内存地址rbp-0x4。
带通话
看上面这个有点长的汇编代码,可以发现每个函数(或者堆栈帧)都有一个ret。
在主要部分,使用了call。现在我们来看看call和ret。
函数调用是如何实现的?汇编中有call语句400592:E8 96 ff ff ff call 40052d func1 call语句表示指令跳转的地址,例如调用40052d func1,但是func 1执行完毕后如何退出调用并返回原处?原来调用call语句的时候会把返回地址(或者当前地址)推到栈顶。调用ret语句时,栈顶的内容,也就是返回地址,会被弹出栈外,从而正确返回main。
Ret从堆栈中弹出返回地址并跳转到那里。当调用一个函数时,程序调用在跳转到被调用函数的第一条指令之前,使用指令按返回地址。
这就是程序如何调用一个函数,然后从该函数返回调用函数以执行其下一条指令。
如下图所示,调用call时,先把要返回的地址压入堆栈。
然后调用func1形成堆栈框架。
其他变量现在回顾本文开头的程序集。
square(int):
推送rbp
mov rbp,rsp
mov DWORD PTR [rbp-4],电子数据交换
移动eax,DWORD PTR [rbp-4]
imul eax,eax
pop rbp
ret里还有两个字符我不理解:edi和eax。
Edi和eax和rbp、rsp一样,都是寄存器的名字。先来澄清一下x86系列寄存器中奇怪的名字。
对寄存器的一个很好的理解是,和软件中的变量一样,它们相当于一种‘硬件变量’。
像C变量一样,寄存器实际上有几种大小:
rax是64位“长”型寄存器。它是在2003年向64位处理器过渡期间添加的。eax是32位的“int”大小寄存器。它是在1985年向带有80386 CPU的32位处理器过渡期间添加的。我习惯使用这种寄存器大小,因为它们也工作在32位模式,尽管我尝试对所有情况都使用较长的rax寄存器。ax是16位的“短”寄存器。它是在1979年与8086 CPU一起添加的,但至今仍用于DOS或BIOS代码中。al和ah是8位“char”大小的寄存器。al是低8位,ah是高8位。它们与1972年8008的8位寄存器非常相似。
X64汇编代码使用16个64位寄存器。此外,其中一些寄存器的低位字节可以作为32位、16位或8位寄存器独立访问。寄存器名称如下
以rax寄存器为例,其结构如下
如上图所示,初始寄存器是8位。比如上图中的8位al在DOS和8086中,8位寄存器扩展为16位ax,在80386中分为高8位ah和低8位al,进一步扩展为32位eax,其中E代表扩展64位处理器使用64位rax,R代表寄存器。再看一遍,上面几段的意思已经很简单了,可以彻底理解了。
参见破解虚拟内存:绘制VM图破解虚拟内存:堆栈、寄存器和汇编代码cs 301x86汇编x64备忘单中的寄存器布朗大学。
来自,
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。