我有这个Hello World示例,它是我用于学习汇编的课程的一部分:
push ebp
mov ebp, esp
push offset aHelloWorld; "Hello world\n"
call ds:__imp__printf
add esp, 4
mov eax, 1234h
pop ebp
retn
此代码由Windows Visual C ++ 2005生成,关闭缓冲区溢出保护并使用IDA Pro 4.9免费版本进行反汇编。
我正在努力了解每条线的作用。
第一行是push ebp
。
我知道ebp
代表基础指针。它的功能是什么?
我在第二行看到esp
中的值被移动到ebp
并在线搜索我看到前两个指令在汇编程序开始时非常常见。
虽然ebp
和esp
在开头是空的?我是装配新手。 ebp
是否用于堆栈帧,所以当我们在代码中有一个函数时,它对于一个简单的程序是可选的吗?
然后push offset aHelloWorld; "Hello world\n"
;
之后的部分是评论,所以它不能被执行吗?第一部分改为将包含字符串Hello World的地址添加到堆栈中,对吧?但是字符串声明在哪里?我不确定我理解。
然后call ds:__imp__printf
它似乎是对函数的调用,无论如何printf
是内置函数吗?
ds
代表数据段注册吗?它是否被使用是因为我们试图访问不在堆栈中的内存操作数?
然后add esp, 4
我们向esp添加4个字节吗?为什么呢?
然后move eax, 1234h
这里的1234h是什么?
然后pop ebx
..它在开头就被推了。是否有必要在最后弹出它?
然后retn
(我知道ret
在调用函数后返回一个值)。我读到retn中的n指的是调用者推送参数的数量。这对我来说不是很清楚。
你能帮我理解吗?
答案 0 :(得分:7)
我试图了解每条线的作用。
这属于学习汇编语言的一般范畴。有关于这个主题的完整书籍;其中一些甚至可能还不错。你应该买一个。为确保您获得最大收益,请务必选择一个专注于您感兴趣的体系结构和操作系统的语言。当然,x86汇编语言总是相同的,但编程模型相当不同在Windows和Linux之间,差异会让初学者感到困惑。
如果您购买书本太便宜,至少阅读Matt Pietrek的经典系列文章," Just Enough Assembly To Get By&来自Microsoft System Journal的#34; 。 Start here,然后前往follow-up。
第一行是
push ebp
。我知道ebp
代表基础指针。它的功能是什么?我在第二行看到
esp
中的值被移入ebp
并在线搜索我看到前两个指令在汇编程序的开头很常见。我是装配新手。
ebp
是否用于堆栈帧,所以当我们在代码中有一个函数时,它对于一个简单的程序是可选的吗?
要孤立地理解第一行,您只需要知道PUSH
指令的作用。它将操作数(在本例中为寄存器)推送到堆栈顶部。 EBP
是几乎总是包含堆栈基指针的寄存器。
但是,这并没有告诉你很多关于此代码的目的的信息。这一行和下一行是standard function prologue的一部分。 Matt在他的第一篇文章的开头附近,在" Procedure Entry and Exit"部分。首先,来自EBP
的堆栈基指针由PUSH
保存到堆栈中。然后,第二条指令将ESP
的值复制到EBP
寄存器中。这使得整个函数中的堆栈交互更容易。通常,序言部分将以一条指令结束,该指令在堆栈上为临时变量保留任意数量的空间(例如,sub esp, 8
以在堆栈上保留8个字节)。这个功能不需要任何。
是的,这个序言代码是可选的。如果您不需要任何堆栈空间和/或使用EBP
- 相对寻址,那么您就不需要标准序言。 Optimizing compilers often omit it如果可能的话。
虽然开始时
ebp
和esp
为空?
不,当然他们不是空的。如果它们为空,则代码无需保存EBP
的值或使用ESP
的值。
实际上, no 寄存器在函数开头是空的。它们包含函数原型(与其调用约定一起)所表示的值,它们包含必须保留的值(也就是说,当函数返回控件时,它们仍必须具有相同的值他们在你的函数第一次被调用时会这样做;这些被称为调用者保存寄存器,它们根据调用约定而不同,或者它们包含你可以认为是垃圾值的东西(这些是 callee-save 寄存器,您可以在被调用函数的代码中自由地删除它们。
然后
push offset aHelloWorld; "Hello world\n"
;
之后的部分是评论,因此它不会被执行吗?第一部分改为将包含字符串Hello World的地址添加到堆栈中,对吧?但是字符串声明在哪里?我不确定我理解。
aHelloWorld
是可执行映像中声明的一段全局数据。它是在链接时放在那里的,可能是因为原始代码使用了字符串文字。该指令PUSH
将该全局数据的offset
(即其地址)添加到堆栈中。
是的,分号后的部分是逗号。反汇编程序将此评论添加为您的帮助。它查找了aHelloWorld
的值,确定它包含字符串Hello world\n
,并将该定义放入内联,从而使您不必自己查找数据的值。< / p>
然后
call ds:__imp__printf
它似乎是对函数的调用,无论如何
printf
是内置函数吗?
是的,CALL
总是调用一个函数。在这种情况下,它正在调用printf
函数。它是一个内置的&#34;功能?这取决于你的定义。从汇编语言的角度来看,没有:没有内置函数。 printf
是C标准库提供的功能。编译和链接原始代码时,也与C运行时库链接,后者提供C标准库函数,包括printf
。由于这是MSVC,__imp__
前缀是一个很大的提示,被调用的函数是标准库或Windows API的一部分。这些是隐式链接的函数。
查找printf
函数表明它需要可变数量的参数。在最常见的x86-32调用约定中,这些参数在堆栈上传递。这就解释了为什么前面的指令PUSH
将字符串数据的地址写入堆栈:它将该地址传递给printf
函数,以便可以将字符串打印到标准输出。它本可以传递给printf
的其他参数,但它没有,因为它不需要:它只需要一个打印文字字符串。
ds
代表数据段注册吗?它是否被使用是因为我们试图访问不在堆栈上的内存操作数?
是的,DS是数据段。你的反汇编程序在这里很冗长。在Windows中,x86-32使用flat memory model,因此您基本上可以完全忽略segment registers,并且仍然能够理解所有正常运行的内容。
然后
add esp, 4
我们向esp添加4个字节吗?为什么呢?
是的,这会为ESP
寄存器增加4个字节。为什么?清理堆栈。回想一下,在CALL
函数printf
之前,您PUSH
在堆栈上编写了一个4字节值(可执行映像中字符串数据的偏移量)。 printf
函数是 variadic (接受可变数量的参数),因此调用者总是负责在调用后清理堆栈。
在这里,您可以考虑向ESP
添加4相当于使用POP
指令弹出堆栈。在x86上,堆栈总是向下增长,因此添加等同于弹出(和推动的反转)。
然后
move eax, 1234h
这里的1234h是什么?
此指令MOV
将常量值0x1234
(h
表示十六进制)加入EAX
寄存器。
为什么呢?好吧,我猜。在所有x86调用约定中,EAX
寄存器包含函数的返回值。因此,该函数的原始代码很可能以return 0x1234;
结束。
然后
pop ebx
..它在开头就被推了。是否有必要在最后弹出它?
实际上,它弹出EBP
,这是在函数开头实际推送的内容。
是的。您PUSH
到堆栈的所有内容都必须从堆栈中POP
。 (或者等同于我们之前在ADD
到ESP
时所看到的。)你必须清理堆栈。这是结语的功能,它对应于我们在开头看到的序言。请参阅马特的文章,其中涉及&#34;程序进入和退出&#34;。
然后
retn
(我知道ret
在调用函数后返回一个值)。我读到retn中的n指的是调用者推送参数的数量。
这只是你的反汇编程序的特殊性。 IDA Pro使用retn
助记符。这实际上意味着 near 返回,但由于x86-32使用平坦(非分段)内存模型,因此近与远的区别是不相关的。您可以将retn
简单地视为等同于ret
。
请注意,这与参数的ret
指令不同,这是您正在考虑的内容。它没有&#34;返回&#34;但是它的论点。该函数在EAX
寄存器中返回其结果。相反,ret n
(其中n
是16字节的立即值)返回并弹出堆栈中指定的字节数。这仅用于某些调用约定(最常见的是__stdcall
),其中被调用者负责清理堆栈。
有关调用约定的详情,请参阅x86 tag wiki和Wikipedia中的链接。
对我来说这不是很清楚。 你能帮我理解吗?
我是否提到过你应该拿一本教授汇编语言编程的书?