返回介绍

2.1 x86

发布于 2025-02-22 14:00:41 字数 5533 浏览 0 评论 0 收藏 0

2.1.1 MSVC-x86

在 MSVC 2010 中编译一下:

#!bash    
cl 1.cpp /Fa1.asm

(/Fa 选项表示生产汇编列表文件)

#!bash
CONST   SEGMENT
$SG3830 DB      'hello, world', 00H
CONST   ENDS
PUBLIC  _main
EXTRN   _printf:PROC
; Function compile flags: /Odtp
_TEXT   SEGMENT
_main   PROC

        push    ebp
        mov     ebp, esp
        push    OFFSET $SG3830
        call    _printf
        add     esp, 4
        xor     eax, eax
        pop     ebp
        ret     0

_main   ENDP 
_TEXT   ENDS

MSVC 生成的是 Intel 汇编语法。Intel 语法与 AT&T 语法的区别将在后面讨论。

编译器会把 1.obj 文件连接成 1.exe。

在我们的例子当中,文件包含两个部分:CONST(放数据)和_TEXT(放代码)。

字符串“hello,world”在 C/C++ 类型为 const char* ,然而他没有自己的名称。

编译器需要处理这个字符串,就自己给他定义了一个$SG3830。

所以例子可以改写为:

#!cpp
#include <stdio.h>
const char *$SG3830="hello, world";
int main() {
    printf($SG3830);
    return 0; 
};

我们回到汇编列表,正如我们看到的,字符串是由 0 字节结束的,这也是 C/C++的标准。

在代码部分, _TEXT ,只有一个函数:main()。

函数 main() 与大多数函数一样都有开始的代码与结束的代码。

函数当中的开始代码结束以后,调用了 printf() 函数: CALL _printf

在 PUSH 指令的帮助下,我们问候语字符串的地址(或指向它的指针)在被调用之前存放在栈当中。

当 printf() 函数执行完返回到 main() 函数的时候,字符串地址(或指向它的指针) 仍然在堆栈中。

当我们都不再需要它的时候,堆栈指针(ESP 寄存器)需要改变。

#!bash
ADD ESP, 4

意思是 ESP 寄存器加 4。

为什么是 4 呢?由于是 32 位的代码,通过栈传送地址刚好需要 4 个字节。

在 64 位系统当中它是 8 字节。

ADD ESP, 4 实际上等同于 POP register

一些编辑器(如 Intel C++编译器)在同样的情况下可能会用 POP ECX 代替 ADD(例如这样的模式可以在 Oracle RDBMS 代码中看到,因为它是由 Intel C++编译器编译的),这条指令的效果基本相同,但是 ECX 的寄存器内容会被改写。

Intel C++编译器可能用 POP ECX ,因为这比 ADD ESP, X 需要的字节数更短,(1 字节对应 3 字节)。

在调用 printf() 之后,在 C/C++代码之后执行 return 0 ,return 0 是 main() 函数的返回结果。

代码被编译成指令 XOR EAX, EAX

XOR 事实上就是异或,但是编译器经常用它来代替 MOV EAX, 0 原因就是它需要的字节更短(2 字节对应 5 字节)。

有些编译器用 SUB EAX, EAX 就是 EXA 的值减去 EAX,也就是返回 0。

最后的指令 RET 返回给调用者,他是 C/C++代码吧控制返还给操作系统。

2.1.2 GCC-x86

现在我们尝试同样的 C/C++代码在 linux 中的 GCC 4.4.1 编译

#!bash
gcc 1.c -o 1

下一步,在 IDA 反汇编的帮助下,我们看看 main() 函数是如何被创建的。

(IDA,与 MSVC 一样,也是显示 Intel 语法)。

我也可以是 GCC 生成 Intel 语法的汇编代码,添加参数

#!bash
-S -masm=intel

汇编代码:

#!bash
main            proc near 

var_10          = dword ptr -10h

                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 10h
                mov     eax, offset aHelloWorld ; "hello, world"
                mov     [esp+10h+var_10], eax
                call _printf
                mov eax, 0
                leave
                retn
main            endp

结果几乎是相同的,“hello,world”字符串地址(保存在 data 段的)一开始保存在 EAX 寄存器当中,然后保存到栈当中。

同样的在函数开始我们看到了

AND ESP, 0FFFFFFF0h

这条指令该指令对齐在 16 字节边界在 ESP 寄存器中的值。这导致堆栈对准的所有值。

SUB ESP,10H 在栈上分配 16 个字节。 这里其实只需要 4 个字节。

这是因为,分配堆栈的大小也被排列在一个 16 字节的边界。

该字符串的地址(或这个字符串指针),不使用 PUSH??指令,直接写入到堆栈空间。 var_10,是一个局部变量,也是 printf() 的参数。

然后调用 printf() 函数。

不像 MSVC,当 gcc 编译不开启优化,它使用 MOV EAX,0 清空 EAX,而不是更短的代码。

最后一条指令,LEAVE 相当于 MOV ESP,EBP 和 POP EBP 两条指令。

换句话说,这相当于指令将堆栈指针(ESP)恢复,EBP 寄存器到其初始状态。

这是必须的,因为我们在函数的开头修改了这些寄存器的值(ESP 和 EBP)(执行 MOV EBP,ESP/AND ESP...)。

2.1.3 GCC:AT & T 语法

我们来看一看在 AT&T 当中的汇编语法,这个语法在 UNIX 当中更普遍。

#!bash
gcc -S 1_1.c

我们将得到这个:

#!bash
.file   "1_1.c" 
.section    .rodata

.LC0:
        .string "hello, world"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        andl    $-16, %esp
        subl    $16, %esp
        movl    $.LC0, (%esp)
        call    printf
        movl    $0, %eax
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
        .section        .note.GNU-stack,"",@progbits

有很多的宏(用点开始)。现在为了简单起见,我们先不看这些。(除了 .string ,就像一个 C 字符串编码一个 null 结尾的字符序列)。然后,我们将看到这个:

#!bash
.LC0:
        .string "hello, world"
main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $16, %esp
        movl    $.LC0, (%esp)
        call    printf
        movl    $0, %eax
        leave
        ret

在 Intel 与 AT&T 语法当中比较重要的区别就是:

操作数写在后面

在 Intel 语法中:<instruction> <destination operand> <source operand>
在 AT&T 语法中:<instruction> <source operand> <destination operand>

有一个理解它们的方法: 当你面对 intel 语法的时候,你可以想象把等号放到 2 个操作数中间,当面对 AT&T 语法的时候,你可以放一个右箭头(→)到两个操作数之间。

AT&T: 在寄存器名之前需要写一个百分号(%) 并且在数字前面需要美元符($)。方括号被圆括号替代。 AT&T: 一些用来表示数据形式的特殊的符号

l      long(32 bits)
w      word(16bits)
b      byte(8 bits)

让我们回到上面的编译结果:它和在 IDA 里看到的是一样的。只有一点不同:0FFFFFFF0h 被写成了 $-16 ​,但这是一样的,10 进制的 16 在 16 进制里表示为 0x10。-0x10 就等同于 0xFFFFFFF0 ​(这是针对于 32 位构架)。

外加返回值这里用的 MOV 来设定为 0,而不是用 XOR。MOV 仅仅是加载(load)了变量到寄存器。指令的名称并不直观。在其他的构架上,这条指令会被称作例如”load”这样的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文