【问题标题】:Understanding Assembly Hello World了解装配 Hello World
【发布时间】:2017-01-16 04:57:32
【问题描述】:

我有这个 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 是指调用者推送的参数的数量。这对我来说不是很清楚。 你能帮我理解吗?

【问题讨论】:

  • 我想知道你为什么要开发漏洞利用程序?当前的 SW 已经很故障了,而且工作多次纯属偶然。如今,通常不通过常规使用来降低它比通过利用来崩溃它要付出更多的努力。害怕做一些真正的编码,所以你想保持简单的一面?
  • 我从事信息安全工作,我也想学习漏洞利用开发。简单的。无论如何,这只是一个无害的问候词示例。你能帮我好好理解一下代码吗?
  • 我正在尝试,但是您有很多问题,而且您对此很陌生,因此返回一些教程和文档以及一些计算机体系结构书籍等可能会更有意义...我正在写一些部分答案,但不要期望太多。
  • 顺便说一句,这个“简单无害的你好世界”看起来像是从动态库中加载printf。因此,如果您在执行此操作期间以某种方式(通过某种攻击)注入恶意 dll,使用已修补的恶意 printf 版本,它可能会造成很多伤害(至少在您运行 hello world 的上下文中,除非该恶意代码使用其他一些错误来提升它的权限并转义当前上下文等......)。 ......“无害”就这么多......:D
  • 甚至还有不同的语法、不同的汇编程序、操作系统和另一个操作系统之间的差异......架构和另一个架构的差异(虽然现在我专注于 x86),所以迷路很容易而且它需要许多先决条件..

标签: assembly x86


【解决方案1】:

我试图了解每一行的作用。

这属于学习汇编语言的一般范畴。关于这个主题有整本书;其中一些可能甚至还不错。你应该买一个。为确保您获得最大的收益,请务必选择一种专注于您感兴趣的架构和操作系统的语言。当然,x86 汇编语言总是相同的,但 Windows 之间的编程模型差异很大和 Linux 的差异会让初学者感到困惑。

如果你买书太便宜,至少阅读来自微软的 Matt Pietrek 的经典系列文章,“Just Enough Assembly To Get By”系统日志。 Start here,然后转到follow-up

第一行是push ebp。我知道ebp 代表基本指针。它的作用是什么?

我看到在第二行中esp 中的值被移动到ebp 并在线搜索我发现前两条指令在汇编程序的开头非常常见。

我是组装新手。 ebp 是否用于堆栈帧,所以当我们的代码中有一个函数并且对于一个简单的程序来说它是可选的吗?

要单独理解第一行,您只需要知道PUSH 指令的作用。它将操作数(在本例中为寄存器)压入栈顶。 EBP 是几乎总是包含栈基指针的寄存器。

不过,这并没有告诉您太多关于此代码的用途。这一行和下一行是standard function prologue 的一部分。 Matt 在他的第一篇文章开头附近的“程序进入和退出”部分谈到了这一点。首先,来自EBP 的堆栈基指针通过PUSH 将其保存到堆栈中。然后,第二条指令将ESP 的值复制到EBP 寄存器中。这使得在整个函数中与堆栈的交互更容易。通常,序言部分将以一条指令结束,该指令在堆栈上为临时变量保留任意数量的空间(例如sub esp, 8 在堆栈上保留 8 个字节)。这个函数不需要。

是的,此序言代码是可选的。如果您不需要任何堆栈空间和/或您使用EBP-相对寻址,那么您不需要标准序言。 Optimizing compilers often omit it 尽可能。

虽然ebpesp 开头是空的?

不,当然它们不是空的。如果它们为空,则代码不会费心保存EBP 的值或使用ESP 的值。

事实上,no 寄存器在函数开始时是空的。它们包含函数原型(连同其调用约定)所说的值,它们包含您必须保留的值(也就是说,当您的函数返回控制权时,它们必须仍然具有相同的值函数首先被调用;这些被称为 caller-save 寄存器,它们根据调用约定而有所不同),或者它们包含您可以假设为垃圾值的内容(这些是 callee-save 注册,您可以在被调用函数的代码中随意破坏它们。

然后push offset aHelloWorld; "Hello world\n"

; 之后的部分是注释,所以它不会被执行,对吧?第一部分将包含字符串 Hello World 的地址添加到堆栈中,对吗?但是字符串在哪里声明?我不确定我是否理解。

aHelloWorld 是在可执行映像中声明的一段全局数据。它是在链接时放在那里的,可能是因为原始代码使用了字符串文字。这条指令PUSHes 将该全局数据(即其地址)的offset 放入堆栈。

是的,分号后面的部分是逗号。反汇编程序正在添加此评论作为对您的帮助。它查找了aHelloWorld 的值,确定它包含字符串Hello world\n,并将该定义置于行内,这样您就不必自己查找数据的值了。

然后call ds:__imp__printf

似乎是对函数的调用,反正printf 是一个内置函数吧?

是的,CALL 总是调用一个函数。在这种情况下,它正在调用printf 函数。它是“内置”功能吗?这取决于你的定义。从汇编语言的角度来看,不:没有内置函数。 printf 是 C 标准库提供的函数。当原始代码被编译和链接时,它与提供C标准库函数的C运行时库链接,包括printf。由于这是 MSVC,__imp__ 前缀是一个重要提示,表明被调用的函数是标准库或 Windows API 的一部分。这些是隐式链接的函数。

查找printf 函数表明它接受可变数量的参数。在最常见的 x86-32 调用约定中,这些参数在堆栈上传递。这就解释了为什么前面的指令PUSHed 将字符串数据的地址放入堆栈:它将该地址传递给printf 函数,以便可以将字符串打印到标准输出。它本可以将其他参数传递给printf,但它没有,因为它不需要:它只需要一个来打印文字字符串。

ds 是否代表数据段寄存器?是否因为我们试图访问不在堆栈上的内存操作数而使用它?

是的,DS 是数据段。您的反汇编程序在这里很冗长。在 Windows 中,x86-32 使用flat memory model,因此您基本上可以完全忽略segment registers,并且仍然可以很好地理解正在发生的一切。

然后add esp, 4

我们是否向 esp 添加 4 个字节?为什么?

是的,这会将 4 个字节添加到 ESP 寄存器。为什么?清理堆栈。回想一下,在CALLprintf 函数之前,您PUSH在堆栈上创建了一个 4 字节的值(可执行映像中字符串数据的偏移量)。 printf 函数是variadic(接受可变数量的参数),所以调用者总是负责在调用后清理堆栈。

在这里,你可以认为ESP 加 4 相当于用POP 指令出栈。在 x86 上,堆栈总是向下增长,因此添加相当于弹出(和推送的逆)。

那么move eax, 1234h这里的1234h是什么?

这条指令MOV将常量值0x1234h表示十六进制)写入EAX寄存器。

为什么?嗯,我能猜到。在所有 x86 调用约定中,EAX 寄存器包含函数的返回值。所以很有可能函数的原代码以return 0x1234;结尾。

然后pop ebx..它在开始时被推送。有必要在最后弹出吗?

其实是弹出了EBP,也就是函数开头实际推送的内容。

是的。你PUSH 入栈的所有东西都必须POP 出栈。 (或等效的,正如我们之前看到的ADDing 到ESP。)您必须清理堆栈。这就是函数epilogue,对应于我们一开始看到的prologue。请参考 Matt 的文章,其中谈到了“过程进入和退出”。

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

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

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

有关调用约定的更多信息,请参阅 tag wikiWikipedia 中的链接。

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

我有没有提到你应该买一本教汇编语言编程的书?

【讨论】:

  • 非常感谢。你的答案很明确,我当然愿意学习组装。我不介意在其中投入时间和金钱。我已经有一本书了,我刚开始学习并开始了一个视频课程,但可能它的速度太快了,我需要先学习更多的基础知识。
  • 我认为许多人在尝试学习汇编时所犯的错误是没有先学习 C(或 C++)。您确实需要首先了解操作系统的调用约定、链接和编程基础知识。没有它,你只是记住了汇编操作数的助记符和它们的功能描述,这只能给你一半的画面。一个足够聪明的反汇编器可以做到这一点(为每条指令添加通用注释)。需要一个人来理解为什么代码会做它所做的事情,尤其是在您进行逆向工程时。
  • @Fabio 也可以在 SO 上查看“汇编”文档,其中包含一般介绍/机器代码/堆栈/寄存器 .. 目前非常有限,因此您将很快完成它。运气好的话,它可能会填满其他信息(或者让你更加困惑)。并且“一开始是空的”是您将 C 概念(通过值副本传递的参数)与寄存器混淆。寄存器是固定的硅芯片,由于您将 CPU 连接到有效电流,因此它们的位中有一些值。它们不能为空,这些位始终具有一定的价值,而且是全球性的。
  • @Ped7g 感谢您的进一步澄清,并建议您查看关于 SO 的程序集文档。我马上就做。
  • 是的,事实证明 DWORD PTR 正是它的作用。如果您将其关闭并组装,您会收到直接电话 (E8 rel32)。我实际上不知道,或者至少没有考虑过。但这是有道理的。 PTR 引入了间接级别,这正是您期望括号做的事情。我想人们确实已经习惯了他熟悉的工具。关于 MASM 语法,我几乎可以说出我将要了解的内容。但其中很多只是对模式的熟悉,而且你几乎总能明白你的意思。
猜你喜欢
  • 2011-09-12
  • 1970-01-01
  • 2011-04-19
  • 1970-01-01
  • 2020-08-21
  • 1970-01-01
  • 2012-01-04
  • 2014-10-15
  • 2023-04-11
相关资源
最近更新 更多