Heyo,
我已经编写了这个非常基本的主要功能来试验反汇编,还可以看到并希望了解较低级别的内容:
int main() {
return 6;
}
使用gdb来显示disas main:
0x08048374 <main+0>: lea 0x4(%esp),%ecx
0x08048378 <main+4>: and $0xfffffff0,%esp
0x0804837b <main+7>: pushl -0x4(%ecx)
0x0804837e <main+10>: push %ebp
0x0804837f <main+11>: mov %esp,%ebp
0x08048381 <main+13>: push %ecx
0x08048382 <main+14>: mov $0x6,%eax
0x08048387 <main+19>: pop %ecx
0x08048388 <main+20>: pop %ebp
0x08048389 <main+21>: lea -0x4(%ecx),%esp
0x0804838c <main+24>: ret
我最好的猜测是我的想法是什么以及我需要逐行帮助:
lea 0x4(%esp),%ecx
将esp + 4的地址加载到ecx中。 为什么我们将4添加到esp?
我在某处读到这是命令行参数的地址。但是当我做x/d $ecx
时,我得到了argc的值。 存储的实际命令行参数值在哪里?
and $0xfffffff0,%esp
对齐堆栈
pushl -0x4(%ecx)
将esp最初的地址推入堆栈。 这是什么目的?
push %ebp
将基指针推入堆栈
mov %esp,%ebp
将当前堆栈指针移动到基指针
push %ecx
将原始esp + 4的地址推入堆栈。的为什么吗
mov $0x6,%eax
我想在这里返回6,所以我猜测返回值存储在eax中?
pop %ecx
将ecx恢复为堆栈上的值。 当我们返回时,为什么我们希望ecx为esp + 4?
pop %ebp
将ebp恢复到堆栈上的值
lea -0x4(%ecx),%esp
将esp恢复为原始值
ret
在装配时我是n00b所以任何帮助都会很棒!此外,如果您发现任何关于我认为发生的事情的错误陈述,请纠正我。
非常感谢! :
答案 0 :(得分:12)
函数体开头的代码:
push %ebp
mov %esp, %ebp
是创建所谓的堆栈帧,它是引用过程本地参数和对象的“坚实基础”。使用%ebp
寄存器(如其名称所示)作为基指针,它指向过程中本地堆栈的 base (或底部)
进入程序后,堆栈指针寄存器(%esp
)通过调用指令指向存储在堆栈中的返回地址(它是紧跟在指令之后的指令的地址)呼叫)。如果你现在只调用ret
,这个地址将从堆栈弹出到%eip
(指令指针),代码将从该地址({{{}之后的下一条指令执行) 1}})。但是我们还没有回来,对吗? ; - )
然后推送call
注册表以保存其先前的值而不会丢失它,因为您很快就会将其用于某些事情。 (顺便说一下,它通常包含调用函数的基指针,当你查看该值时,你会找到一个先前存储的%ebp
,它将再次成为高一级函数的基指针,所以你可以通过这种方式跟踪调用堆栈。)当您保存%ebp
时,可以在那里存储当前的%ebp
(堆栈指针),以便%esp
指向相同的地址:当前本地堆栈的基础。当您要在堆栈上推送和弹出值或保留&amp;时,%ebp
将在程序内来回移动。释放局部变量。但是%esp
将保持固定,仍然指向本地堆栈帧的基础。
调用者传递给过程的参数“只是在地面上埋葬”(也就是说,它们相对于基数具有正偏移量,因为堆栈增长了)。您在%ebp
中有本地堆栈的基址,其中%ebp
的前一个值。它下面(即%ebp
位于返回地址。因此,第一个参数位于4(%ebp)
,第二个位于8(%ebp)
,依此类推。
局部变量可以在基础上方的堆栈上分配(也就是说,它们相对于基础具有负偏移量)。只需将N减去12(%ebp)
,你就可以在堆栈上为局部变量分配%esp
个字节,方法是将堆栈的顶部移到上面(或者,正好在下面)这个区域:-)你可以通过相对于N
的否定偏移来引用此区域,即%ebp
是第一个单词,-4(%ebp)
是第二个等等。请记住-8(%ebp)
指向本地堆栈的基础,其中已保存先前的(%ebp)
值。因此,在尝试在过程结束时恢复%ebp
到%ebp
之前,请记住将堆栈恢复到之前的位置。你可以用两种方式做到:
1.您可以通过将pop %ebp
添加回N
(堆栈指针)来释放局部变量,也就是说,移动堆栈的顶部就像这些局部变量从未出现过一样。 (好吧,他们的价值观会保留在筹码上,但是他们会被认为是“被释放”,可能会被后续的推动所覆盖,所以引用他们就不再安全了。他们是尸体; -J)
2.您只需将%esp
从先前已修复的%esp
恢复到堆栈基础,即可将堆栈向下冲到地面并释放所有本地空间。它会将堆栈指针恢复到刚进入过程后的状态,并将%ebp
保存到%esp
。这就像加载以前保存的游戏一样,当你弄乱了某些东西时; - )
通过添加开关%ebp
,可以从gcc -S
获得一个不那么混乱的程序集。它告诉GCC不要组装任何用于设置/重置堆栈帧的代码,直到确实需要它为止。请记住,它可能会混淆调试器,因为它们通常依赖于那里的堆栈帧来跟踪调用堆栈。但是如果你不需要调试这个二进制文件,它就不会破坏任何东西。它对于发布目标来说非常好,它可以节省一些时空。
有时你可以遇到一些奇怪的汇编程序指令,从-fomit-frame-pointer
开始与函数头交错。这是所谓的呼叫帧信息。调试器使用它来跟踪函数调用。但它也用于高级语言中的异常处理,这需要堆栈展开和其他基于调用堆栈的操作。您也可以通过添加开关.cfi
在装配中将其关闭。这告诉GCC使用普通旧标签而不是那些奇怪的-fno-dwarf2-cfi-asm
指令,并在程序集的末尾添加一个特殊的数据结构,引用这些标签。这不会关闭CFI,只需将格式更改为“透明”格式:然后程序员可以看到CFI表。
答案 1 :(得分:4)
你的解释做得很好。调用函数时,返回地址会自动推送到堆栈,这就是第一个参数argc被推回到4(%esp)的原因。 argv将从8(%esp)开始,每个参数都有一个指针,后跟一个空指针。此函数将%esp的旧值推送到堆栈,以便它可以包含返回时的原始未对齐值。返回时%ecx的值无关紧要,这就是为什么它被用作%esp引用的临时存储。除此之外,你对一切都是正确的。
答案 2 :(得分:3)
关于你的第一个问题(存储命令行参数的位置),函数的参数就在ebp
之前。我必须说,你的“真实”主要从< main + 10 >
开始,推动ebp
并将esp
移至ebp
。我认为gcc会把所有lea
用来代替函数调用之前和之后esp
上的常用操作(成瘾和减法)。通常一个例程看起来像这样(我做的简单函数):
0x080483b4 <+0>: push %ebp
0x080483b5 <+1>: mov %esp,%ebp
0x080483b7 <+3>: sub $0x10,%esp # room for local variables
0x080483ba <+6>: mov 0xc(%ebp),%eax # get arg2
0x080483bd <+9>: mov 0x8(%ebp),%edx # and arg1
0x080483c0 <+12>: lea (%edx,%eax,1),%eax # just add them
0x080483c3 <+15>: mov %eax,-0x4(%ebp) # store in local var
0x080483c6 <+18>: mov -0x4(%ebp),%eax # and return the sum
0x080483c9 <+21>: leave
0x080483ca <+22>: ret
也许你已经启用了一些优化,这可能会使代码变得更加棘手。
最后是,返回值存储在eax
中。无论如何,你的解释是完全正确的。
答案 3 :(得分:1)
我认为你原来的问题中唯一突出的是为什么你的代码中存在以下陈述:
0x08048381 <main+13>: push %ecx
0x08048382 <main+14>: mov $0x6,%eax
0x08048387 <main+19>: pop %ecx
%ecx在<main+13>
和<main+19>
的推送和弹出似乎没有多大意义 - 他们在这个示例中并没有真正做任何事情,但请考虑您的代码调用函数调用的情况。
系统无法保证对其他功能的调用 - 这将设置自己的堆栈激活帧 - 不会重置寄存器值。事实上,他们可能会。因此,代码在堆栈上设置保存的寄存器部分,其中代码使用的任何寄存器(除了%esp和%ebp之外已经通过常规堆栈设置保存)都存储在堆栈中可能将控制权移交给当前代码块的“meat”中的函数调用。
当这些潜在的呼叫返回时,系统会弹出堆栈中的值以恢复预呼叫寄存器值。如果您直接编写汇编程序而不是编译,那么您自己负责存储和检索这些寄存器值。
但是,对于示例代码,没有函数调用 - 只有<main+14>
处的单个指令,您要设置返回值,但编译器无法知道,并保留其像往常一样注册。
如果您添加了在<main+14>
之后将其他值推送到堆栈的C语句,那么看看会发生什么会很有趣。如果我认为这是堆栈的保存的寄存器部分,那么您希望编译器在pop
之前插入自动<main+19>
语句以清除这些值。