Stack
:堆栈也可以引用 Networking中的TCP/IP堆栈。 本文讨论体系结构中使用的数据结构和堆栈。
“堆栈”是一种数据结构。 您可以分别将元素推送到和从中弹出。 但是,与FIFO(先进先出)不同,从堆栈中弹出的元素是您最后推送的元素。 因此,堆栈也称为后进先出或FILO(先进先出)。
在 X86架构和许多其他架构中,有一个用于代码执行的堆栈。 它用于在调用例程时存储返回指针,但也可以在其上存储临时数据和局部变量。
堆栈理论
许多语言和体系结构都有一个可供使用的堆栈。 当返回值存储在上面时,堆栈帧的概念就出现了。 堆栈被划分为多个堆栈帧。 每个堆栈帧包含例程的本地/临时数据、参数和前一个例程(调用者)的返回值。
X86体系结构上的堆栈示例
在X86体系结构上,堆栈向下增长。 堆栈帧具有关于调用约定的特定结构。 CDECL呼叫约定是使用最广泛的。 它很可能由编译器使用。 使用两个寄存器:
- “”“ESP:“””扩展堆栈指针。 包含“栈顶”地址的32位值(更准确地说是X86上的“栈底”)
- EBP:扩展基指针。 使用CDECL调用约定时定义当前堆栈帧的32位值。 它指向当前的本地数据。 它还可以访问例程参数。
在实现内核时要小心。 如果使用segmentation,则应将DS段配置为其基址与SS相同。 否则,在将指向局部变量的指针传递到函数中时,您可能会遇到问题,因为普通GPRs无法以您可能认为的方式访问堆栈。
下面是一个示例堆栈。 这些元素是保护模式下的4字节字:
内存地址:堆栈元素:
+----------------------------+
0x105000 | Parameter 1 for routine 1 | \ +----------------------------+ | 0x104FFC | First callers return addr. | > Stack frame 1 +----------------------------+ | 0x104FF8 | First callers EBP | / +----------------------------+ 0x104FF4 +->| Parameter 2 for routine 2 | \ <-- Routine 1's EBP | +----------------------------+ | 0x104FF0 | | Parameter 1 for routine 2 | | | +----------------------------+ | 0x104FEC | | Return address, routine 1 | | | +----------------------------+ | 0x104FE8 +--| EBP value for routine 1 | > Stack frame 2 +----------------------------+ | 0x104FE4 +->| Local data | | <-- Routine 2's EBP | +----------------------------+ | 0x104FE0 | | Local data | | | +----------------------------+ | 0x104FDC | | Local data | / | +----------------------------+ 0x104FD8 | | Parameter 1 for routine 3 | \ | +----------------------------+ | 0x104FD4 | | Return address, routine 2 | | | +----------------------------+ > Stack frame 3 0x104FD0 +--| EBP value for routine 2 | | +----------------------------+ | 0x104FCC +->| Local data | / <-- Routine 3's EBP | +----------------------------+ 0x104FC8 | | Return address, routine 3 | \ | +----------------------------+ | 0x104FC4 +--| EBP value for routine 3 | | +----------------------------+ > Stack frame 4 0x104FC0 | Local data | | <-- Current EBP +----------------------------+ | 0x104FBC | Local data | / +----------------------------+ 0x104FB8 | | <-- Current ESP \/\/\/\/\/\/\/\/\/\/\/\/\/\/
The CDECL calling convention is described here:
- Caller's responsibilities
- Push parameters in reverse order (last parameter pushed first)
- Perform the call
- Pop the parameters, use them, or simply increment ESP to remove them (stack clearing)
- The return value is stored in EAX
- Callee's responsibilities (callee is the routine being called)
- Store caller's EBP on the stack
- Save current ESP in EBP
- Code, storing local data on the stack
- For a fast exit load the old ESP from EBP, else pop local data elements
- Pop the old EBP and return – store return value in EAX
It looks like this in assembly (NASM):
SECTION .text
caller:
; ... ; Caller responsibilities: PUSH 3 ; push the parameters in reverse order PUSH 2 CALL callee ; perform the call ADD ESP, 8 ; stack cleaning (remove the 2 words) ; ... Use the return value in EAX ...
callee:
; Callee responsibilities: PUSH EBP ; store caller's EBP MOV EBP, ESP ; save current stack pointer in EBP ; ... Code, store return value in EAX ... ; Callee responsibilities: MOV ESP, EBP ; remove an unknown number of local data elements POP EBP ; restore caller's EBP RET ; return
The GCC compiler does all this automatically, but if you have to call C/C++ methods from assembly or reverse, you have to know the convention. Now look at one stack frame (the callee's):
+-------------------------+ | Parameter 3 | +-------------------------+ | Parameter 2 | +-------------------------+ | Parameter 1 | +-------------------------+ | Caller's return address | +-------------------------+ | Caller's EBP value | +-------------------------+ | Local variable | <-- Current EBP +-------------------------+ | Local variable | +-------------------------+ | Local variable | +-------------------------+ | Temporary data | +-------------------------+ | Temporary data | +-------------------------+ | | <-- Current ESP +-------------------------+
Using EBP the callee can access both parameters and local variables:
MOV EAX, [[EBP + 12]] ; Load parameter 1 into EAX
MOV EAX, [[EBP + 16]] ; Load parameter 2
MOV EAX, [[EBP + 4 * EBX + 12]] ; Load parameter EBX (0-indexed)
MOV EAX, [[EBP]] ; Load local variable 1
MOV EAX, [[EBP - 4]] ; Load local variable 2
X86还有其他调用约定。举几个例子:Pascal调用约定、fastcall约定、stdcall。 更多关于维基百科的信息,请参阅下面的链接。
设置堆栈
创建内核时,必须手动设置堆栈。
如果从实模式转到保护模式,还必须设置堆栈。 这是因为SS段可能会发生变化,因此受保护模式下的ESP不会指向与实际模式下的SP相同的位置。 如果您在实模式和保护模式之间切换很多,那么它们可以共享堆栈。 你必须自己找到一个聪明的解决方案。 这是可以做到的。
在保护模式下,只需将新指针值移动到ESP寄存器中即可设置堆栈:
MOV ESP,0x105000;设置堆栈指针
记住,它是向下生长的。 您可以在内核的中为它分配空间。BSS部分,如果它包含一个:
SECTION .text
set_up_stack:
MOV ESP, stack_end ; Set the stack pointer
SECTION .bss
stack_begin: RESB 4096 ; Reserve 4 KiB stack space stack_end:
如果您的内核是由Multiboot兼容的引导加载程序引导的,比如GRUB,则会向您提供一个内存映射。 您可以通过查找适当大小的空闲内存块来设置堆栈。 您只需确保在设置堆栈指针时不会覆盖任何重要数据或代码。
安全
堆栈很容易使用,但有一个问题。 没有“结束”,所以它容易受到缓冲区溢出攻击的变化。 攻击者推送的元素超过堆栈所能容纳的数量,因此元素被推送到堆栈内存之外,从而覆盖代码,然后攻击者可以执行这些代码。
在X86保护模式下,可以通过仅为堆栈分配 GDT描述符来解决此问题,该描述符定义了堆栈的边界。
堆栈跟踪
调试时,通常会显示堆栈跟踪,这很有帮助。 Stack Trace介绍了如何实现这一点,并使用上面的堆栈布局为X86 CDECL提供了示例代码。
展开堆栈
展开堆栈很复杂。它是在使用异常时完成的,比如在C++中。它是在抛出异常时执行的。展开堆栈的目的是调用堆栈帧的本地对象的析构函数,并移除堆栈帧,直到找到合适的平台。着陆台是最好的选择。。catch块在C++或java中。catch块必须与异常匹配,即RuntimeException对象不能作为String对象捕获。
退绕算法取决于体系结构。 通常,该算法在语言运行库中提供。 当使用GCC和C++时,它在与应用程序链接的LIbSuc++库中定义。 但是,在创建内核时不会发生这种情况。 libsupc++库也过于臃肿,无法在内核空间中使用。
另见
文章
Stack Trace - Trace the called functions from the stack
线程
外部链接
- x86 calling conventions on Wikipedia.
- Stack (data structure) on Wikipedia.