我有这个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
Run Code Online (Sandbox Code Playgroud)
此代码由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指的是调用者推送参数的数量.这对我来说不是很清楚.你能帮我理解吗?
我试图了解每一行的作用.
这将属于学习汇编语言的一般类别.有关于这个主题的完整书籍; 其中一些甚至可能还不错.你应该买一个.为确保您获得最大收益,请务必选择一个专注于您感兴趣的体系结构和操作系统的语言.当然,x86汇编语言总是相同的,但Windows之间的编程模型差异很大和Linux的差异会让初学者感到困惑.
如果你买一本书太便宜,至少可以阅读Matt Pietrek的经典系列文章,"只需要足够的装配",来自Microsoft System Journal.从这里开始,然后继续进行后续工作.
第一行是
push ebp
.我知道ebp
代表基指针.它的功能是什么?我看到在第二行中,值
esp
被移入ebp
并在线搜索我看到前两个指令在汇编程序的开头很常见.我是装配新手.被
ebp
用于堆栈帧,所以当我们在我们的代码有一个功能是可选的一个简单的程序?
要孤立地理解第一行,您只需要知道PUSH
指令的作用.它将操作数(在本例中为寄存器)推送到堆栈顶部.EBP
是几乎总是包含堆栈基指针的寄存器.
但是,这并没有告诉你很多关于这段代码的目的.这一行和下一行是标准函数序言的一部分.Matt在他的第一篇文章的开头,"程序进入和退出"部分中谈到了这一点.首先,堆栈基指针从中EBP
保存PUSH
到堆栈中.然后,第二条指令将值复制ESP
到EBP
寄存器中.这使得整个函数中的堆栈交互更容易.通常,序言部分将以一条指令结束,该指令在堆栈上为临时变量保留任意数量的空间(例如,sub esp, 8
在堆栈上保留8个字节).这个功能不需要任何功能.
是的,这个序言代码是可选的.如果您不需要任何堆栈空间和/或使用EBP
-relative寻址,那么您不需要标准序言.优化编译器通常会在可能的情况下省略它.
虽然是
ebp
和esp
一开始是空的?
不,当然他们不是空的.如果它们是空的,代码将不会费心保存EBP
或使用值的值ESP
.
实际上,函数开头没有寄存器是空的.它们包含函数的原型(与其调用约定一起)所表示的值,它们包含您必须保留的值(也就是说,当您的函数返回它们在您的函数返回的控件时,它们仍必须具有相同的值函数首先被调用;这些被称为调用者保存寄存器,它们根据调用约定而不同,或者它们包含你可以认为是垃圾值的东西(这些是被调用者保存寄存器,你可以自由地在被调用函数的代码中破坏它们).
然后
push offset aHelloWorld; "Hello world\n"
之后的部分
;
是评论,所以它不会被执行吗?第一部分改为将包含字符串Hello World的地址添加到堆栈中,对吧?但是字符串声明在哪里?我不确定我理解.
aHelloWorld
是可执行映像中声明的一段全局数据.它是在链接时放在那里的,可能是因为原始代码使用了字符串文字.该指令PUSH
ES中的offset
全局数据的(即,它的地址)压入堆栈.
是的,分号后的部分是逗号.反汇编程序将此评论添加为您的帮助.它查找了它的值aHelloWorld
,确定它包含字符串Hello world\n
,并将该定义放入内联,从而使您不必自己查找数据的值.
然后
call ds:__imp__printf
它似乎是对函数的调用,无论如何
printf
都是内置函数吗?
是的,CALL
总是调用一个函数.在这种情况下,它正在调用该printf
函数.它是"内置"功能吗?这取决于你的定义.从汇编语言的角度来看,没有:没有内置函数.printf
是C标准库提供的功能.当原始代码被编译和链接时,它也与C运行时库链接,后者提供C标准库函数,包括printf
.由于这是MSVC,因此__imp__
前缀是一个很大的提示,即被调用的函数是标准库或Windows API的一部分.这些是隐式链接的函数.
查找printf
函数表明它需要可变数量的参数.在最常见的x86-32调用约定中,这些参数在堆栈上传递.这就解释了为什么前面的指令PUSH
将字符串数据的地址写入堆栈:它将该地址传递给printf
函数,以便可以将字符串打印到标准输出.它可能已经传递了额外的参数printf
,但它没有,因为它不需要:它只需要一个打印文字字符串.
并且
ds
代表数据段寄存器吗?它是否被使用是因为我们试图访问不在堆栈上的内存操作数?
是的,DS是数据段.你的反汇编程序在这里很冗长.在Windows中,x86-32使用平面内存模型,因此您基本上可以完全忽略段寄存器,并且仍然可以很好地理解所有正在进行的操作.
然后
add esp, 4
我们向esp添加4个字节吗?为什么?
是的,这会给ESP
寄存器增加4个字节.为什么?清理堆栈.回想一下,在执行CALL
该printf
函数之前,您PUSH
在堆栈上编写了一个4字节的值(可执行映像中的字符串数据的偏移量).该printf
函数是可变参数(采用可变数量的参数),因此调用者总是负责在调用后清理堆栈.
在这里,你可以想到添加4 ESP
相当于用POP
指令弹出堆栈.在x86上,堆栈总是向下增长,因此添加等同于弹出(和推动的反向).
那么
move eax, 1234h
1234h在这里是什么?
该指令MOV
将常量值0x1234
(h
十六进制值)加到EAX
寄存器中.
为什么?好吧,我猜.在所有x86调用约定中,EAX
寄存器包含函数的返回值.所以函数的原始代码很可能以#结尾return 0x1234;
.
然后
pop ebx
..在开始时推了推.是否有必要在最后弹出它?
实际上,它会弹出EBP
,这是在函数开头实际推送的内容.
是的.你PUSH
进入堆栈的所有东西都必须POP
从堆栈中删除.(或等价物,正如我们前面所看到的与ADD
荷兰国际集团到ESP
.)你必须清理堆栈.这是与我们在开头看到的序幕相对应的功能结尾.请参阅Matt的文章,其中讨论了"程序进入和退出".
然后
retn
(我知道ret
在调用函数后返回一个值).我读到retn中的n指的是调用者推送参数的数量.
这只是你的反汇编程序的特殊性.IDA Pro使用retn
助记符.这实际上意味着接近返回,但由于x86-32使用平坦(非分段)内存模型,因此近与远的区别是不相关的.你可以认为retn
只是等同于ret
.
请注意,这与ret
采用参数的指令不同,这是您正在考虑的.但它并没有"回归"它的论点.该函数将其结果返回到EAX
寄存器中.相反,ret n
(其中n
16字节的立即值)返回并弹出堆栈中指定的字节数.这仅用于某些调用约定(最常见__stdcall
),其中被调用者负责清理堆栈.
有关调用约定的更多信息,请参阅x86 标记wiki和Wikipedia中的链接.
这对我来说不是很清楚.你能帮我理解吗?
我是否提到过你应该得到一本教授汇编语言编程的书?
归档时间: |
|
查看次数: |
995 次 |
最近记录: |