Linux系统中堆栈的ESP/EIP解析
在计算机系统中,栈是一种常见的数据结构,常作为程序的运行空间之一,其中堆栈是最常用的一种。Linux系统中的栈是用C语言实现的,并且在内存中以“后进先出”的方式进行存储和管理。在程序运行时,栈被用来保存函数调用的返回地址和局部变量,还用来作为内存缓存圈和临时存储区域。在本文中,将详细分析Linux系统中堆栈的ESP/EIP解析。
ESP是指栈指针寄存器,EIP是指指令指针寄存器,它们都是CPU寄存器中的一种。ESP通常用来记录栈的栈顶位置,而EIP则用来记录程序将要执行的下一条指令的地址。它们在一起被用来为CPU提供一个基于栈的函数调用机制。
当程序执行到函数调用指令时,会将当前指令的地址压入栈中,同时将ESP指针向下移动,指向新的栈顶。接着程序会跳转到指定的函数地址,并继续执行函数中的代码。在函数执行过程中,栈会被用来存放函数的参数和局部变量。在函数执行完毕后,原来的栈顶地址被弹出,同时ESP指针向上移动,指向新的栈顶。此时,程序会继续执行上一层函数中的代码,通过EIP指针跳转到之前保存的返回地址中。
在多层函数调用时,栈会不断地被创建和销毁,同时ESP和EIP指针也会有相应的变化。当一个函数A调用另一个函数B时,程序必须将A函数的ESP和EIP指针保存下来,以便在B函数执行完毕后,能够正确地返回到A函数中。这种保存ESP和EIP指针的机制称为栈帧,是用来管理栈中各个函数调用过程的重要数据结构。
在Linux系统中,ESP和EIP指针的值是由操作系统内核进行管理的。当一个程序通过系统调用向操作系统请求分配内存时,内核会自动将堆栈分配给该程序,并分配相应的ESP和EIP寄存器。同时,内核还会为程序分配一个初始的栈帧,用来存储程序执行过程中的基本信息。随着程序的执行,内核会根据需要动态地创建和销毁栈帧,并更新ESP和EIP指针的值。这样,程序在运行过程中就可以访问正确的栈空间,同时能够正确地返回到上一层函数中。
在分析Linux系统中的ESP/EIP解析时,需要注意的是,ESP指针向下移动表示栈顶向下移动,而EIP指针向上移动表示程序将要执行的下一条指令向上移动。它们在整个程序执行过程中都是相对变化的,而不是绝对变化的。因此,在编写Linux程序时,需要明确掌握栈和栈帧的相关知识,正确使用ESP和EIP指针,以确保程序能够正确地运行和返回。
综上所述,Linux系统中的ESP和EIP指针是用来管理栈和栈帧的重要寄存器。它们在程序的函数调用和返回过程中发挥重要的作用,是程序正确执行的关键。在编写Linux程序时,需要充分理解ESP和EIP指针在程序中的作用,合理管理栈和栈帧,以确保程序能够正确运行。
相关问题拓展阅读:
C/C++函数参数的传递方式有三种:值传递(pass by value)、指针传递(pass bypointer)、引用传递(pass by reference)。
C/C++函数参数的传递通道是通过堆栈传递,默认遵循弊改__cdecl(C声明方式),参数由改档调用者从右往左逐个压入堆栈,在函数调用完成之后再由调用者恢复堆栈。(Win32API遵循stdcall传参规范的,不在本文讨论范围)
下面是测试代码
void Swap(__int64* _pnX, __int64* _pnY)
{
__int64 nTemp = *_pnX;
*_pnX = *_pnY;
*_pnY = nTemp;
}
void Swap(__int64& _nX, __int64& _nY)
{
__int64 nTemp = _nX;
_nX = _nY;
_nY = nTemp;
}
void SetValue(__int64 _nX)
{
__int64 nTemp = _nX;
}
// Test001
void GetMemory(__int64* _pBuff)
{
_pBuff = new __int64;
}
// Test002
void GetMemory(__int64** _ppBuff)
{
*_ppBuff = new __int64;
}
int _tmain(int argc, _TCHAR* argv)
{
__int64 nA = 0x10;
__int64 nB = 0x20;
// Test to pass by pointer
Swap(&nA, &nB);
// Test to pass by reference
Swap(nA, nB);
// Test to pass by value
SetValue(nA);
// Test the pointer that points the pointer
__int64* _pArray = NULL;
GetMemory(&_pArray);
delete _pArray;
_pArray = NULL;
//租歼判 Test the pointer
GetMemory(_pArray);
return 0;
}
指针传递和引用传递
// 下面看一下对应的反汇编的代码(VS版)
__int64 nA = 0x10;
E movdword ptr ,10h
movdword ptr ,0
__int64 nB = 0x20;
C movdword ptr ,20h
movdword ptr ,0
// Test to pass by pointer
Swap(&nA, &nB);
A leaeax,
D pusheax
E leaecx,
pushecx
callSwap (4111E5h)
addesp,8
// Test to pass by reference
Swap(nA, nB);
A leaeax,
D pusheax
E leaecx,
pushecx
callSwap (4111E0h)
addesp,8
// GCC版
0x: lea eax,
0x: mov DWORD PTR ,eax
0xa : lea eax,
0xe : mov DWORD PTR ,eax
0x: call 0x
0x: lea eax,
0xa : mov DWORD PTR ,eax
0xe : lea eax,
0x004015a2 : mov DWORD PTR ,eax
0x004015a5 : call 0x
通过上面的反汇编代码,我们可以看出指针传递和引用传递在机制是一样的,都是将指针值(即地址)压入栈中,调用函数,然后恢复栈。Swap(nA, nB)和Swap(&nA, &nB);在实际上的汇编代码也基本上一模一样,都是从栈中取出地址来。由此可以看出引用和指针在效率上是一样的。这也是为什么指针和引用都可以达到多态的效果。指针传递和引用传递其实都是改变的地址指向的内存上的值来达到修改参数的效果。
值传递
下面是值传递对应的反汇编代码
// Test to pass by value
SetValue(nA);
A moveax,dword ptr
D pusheax
E movecx,dword ptr
pushecx
callSetValue (4111EAh)
addesp,8
因为我的机器是32位的CPU,从上面的汇编代码可以看64Bit的变量被分成2个32Bit的参数压入栈中。这也是我们常说的,值传递会形成一个拷贝。如果是一个自定义的结构类型,并且有很多参数,那么如果用值传递,这个结构体将被分割为非常多个32Bit的逐个拷贝到栈中去,这样的参数传递效率是非常慢的。所以结构体等自定义类型,都使用引用传递,如果不希望别人修改结构体变量,可以加上const修饰,如(const MY_STRUCT& _value);
下面来看一下Test001函数对应的反汇编代码的参数传递
__int64* _pArray = NULL;
004137E0 movdword ptr ,0
// Test the pointer
GetMemory(_pArray);
moveax,dword ptr
pusheax
callGetMemory (411203h)
B addesp,4
从上面的汇编代码可以看出,其实是0被压入到栈中作为参数,所以GetMemory(_pArray)无论做什么事,其实都与指针变量_pArray无关。GetMemory()分配的空间是让栈中的临时变量指向的,当函数退出时,栈得到恢复,结果申请的空间没有人管,就产生内存泄露的问题了。《C++ Primer》将参数传递分为引用传递和非引用传递两种,非引用传递其实可以理解为值传递。这样看来,指针传递在某种意义上也是值传递,因为传递的是指针的值(1个4BYTE的值)。值传递都不会改变传入实参的值的。而且普通的指针传递其实是改变的指针变量指向的内容。
下面再看一下Test002函数对应的反汇编代码的参数传递
__int64* _pArray = NULL;
004137E0 movdword ptr ,0
GetMemory(&_pArray);
004137E7 leaeax,
004137EA pusheax
004137EB callGetMemory (4111FEh)
004137F0 addesp,4
从上面的汇编代码lea eax, 可以看出,_pArray的地址被压入到栈中去了。
然后看一看GetMemory(&_pArray)的实现汇编代码。
0xb :push ebp
0xc :mov ebp,esp
0xe :sub esp,0x18
0x004015a1 :mov DWORD PTR ,0x20
0x004015a8 :call 0x473ef0
0x004015ad :mov edx,DWORD PTR
0x004015b0 :mov DWORD PTR ,eax
0x004015b2 :leave
0x004015b3 :ret
蓝色的代码是分配临时变量空间,然后调用分配空间函数分配空间,得到的空间指针即eax.
然后红色的汇编代码即从ebp+0x8的栈上取到上面压入栈中的参数_pArray的地址.
mov DWORD PTR ,eax即相当于把分配的空间指针eax让edx指向,也即让_pArray指向分配的空间eax.
用户态和内核态的转换
1)用户态切换到内核态的3种方式
a. 系统调用 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。系统调用实质上是一个中断,而汇编指令int 就可以实现用户态向内核态切换,iret实现内核态向用户态切换 b. 异常 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换慎嫌的过程自然也就发生银孝慧了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。 这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
2)具体的切换操作 从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中锋答断的处理机制基本上也是一致的,关于它们的具体区别这里不再赘述。关于中断处理机制的细节和步骤这里也不做过多分析,涉及到由用户态切换到内核态的步骤主要包括:
从当前进程的描述符中提取其内核栈的ss0及esp0信息。
使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个 过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的
寄存器
,开始 执行中断处理程序,这时就转到了内核态的程序执行了。
关于linux堆栈espeip的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。
香港服务器选创新互联,2H2G首月10元开通。
创新互联(www.cdcxhl.com)互联网服务提供商,拥有超过10年的服务器租用、服务器托管、云服务器、虚拟主机、网站系统开发经验。专业提供云主机、虚拟主机、域名注册、VPS主机、云服务器、香港云服务器、免备案服务器等。
网页题目:Linux系统中堆栈的espeip解析(linux堆栈espeip)
本文URL:http://www.shufengxianlan.com/qtweb/news37/467837.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联