了解程序集Hello World

时间:2016-09-08 09:56:10

标签: assembly x86

我有这个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并在线搜索我看到前两个指令在汇编程序开始时非常常见。

虽然ebpesp在开头是空的?我是装配新手。 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指的是调用者推送参数的数量。这对我来说不是很清楚。 你能帮我理解吗?

1 个答案:

答案 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如果可能的话。

  

虽然开始时ebpesp为空?

不,当然他们不是空的。如果它们为空,则代码无需保存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将常量值0x1234h表示十六进制)加入EAX寄存器。

为什么呢?好吧,我猜。在所有x86调用约定中,EAX寄存器包含函数的返回值。因此,该函数的原始代码很可能以return 0x1234;结束。

  

然后pop ebx ..它在开头就被推了。是否有必要在最后弹出它?

实际上,它弹出EBP,这是在函数开头实际推送的内容。

是的。您PUSH到堆栈的所有内容都必须从堆栈中POP。 (或者等同于我们之前在ADDESP时所看到的。)你必须清理堆栈。这是结语的功能,它对应于我们在开头看到的序言。请参阅马特的文章,其中涉及&#34;程序进入和退出&#34;。

  

然后retn(我知道ret在调用函数后返回一个值)。我读到retn中的n指的是调用者推送参数的数量。

这只是你的反汇编程序的特殊性。 IDA Pro使用retn助记符。这实际上意味着 near 返回,但由于x86-32使用平坦(非分段)内存模型,因此近与远的区别是不相关的。您可以将retn简单地视为等同于ret

请注意,这与参数ret指令不同,这是您正在考虑的内容。它没有&#34;返回&#34;但是它的论点。该函数在EAX寄存器中返回其结果。相反,ret n(其中n是16字节的立即值)返回并弹出堆栈中指定的字节数。这仅用于某些调用约定(最常见的是__stdcall),其中被调用者负责清理堆栈。

有关调用约定的详情,请参阅 tag wikiWikipedia中的链接。

  

对我来说这不是很清楚。   你能帮我理解吗?

我是否提到过你应该拿一本教授汇编语言编程的书?