【问题标题】:Understanding and analyzing assembly code理解和分析汇编代码
【发布时间】:2018-12-30 22:34:17
【问题描述】:

谁能帮我理解这个汇编代码?我对汇编语言完全陌生,我只是想不通...... 下面的汇编代码应该产生这个函数:

func(int a) { 返回一个 * 34 }

cmets //是我的想法应该是什么意思,如果我错了请纠正我

//esp = stack-pointer, ebp = callee saved, eax = return value

pushl %ebp                   // a is pushed on stack
movl %esp,%ebp               // a = stackpointer
movl 8(%ebp),%eax            // eax = M(8 + a).But what is in M(8 + a)?
sall $4,%eax                 // eax << 4
addl 8(%ebp),%eax            // eax = M(8 + a)
addl %eax,%eax               // eax = eax + eax
movl %ebp,%esp               // eax = t
popl %ebp                    // pop a from stack
ret

有人可以解释一下如何解决这个问题吗?非常感谢!

【问题讨论】:

  • 前两行与a无关。他们只是设置了堆栈框架。 8(%ebp)a

标签: assembly x86


【解决方案1】:
pushl %ebp                   // a is pushed on stack
movl %esp,%ebp               // a = stackpointer

如评论中所述,ebpa 无关。 ebp 是堆栈基指针——这段代码将ebp 的旧值保存到堆栈中,然后将堆栈指针保存在ebp 中。

movl 8(%ebp),%eax            // eax = M(8 + a).But what is in M(8 + a)?

正确。入栈的是eax的输入值。

sall $4,%eax                 // eax << 4

正确。 (并将结果分配回eax。)

addl 8(%ebp),%eax            // eax = M(8 + a)

不,你误解了这一点。这会将堆栈上的值8(ebp)a 的原始值)添加到eax。加法应用于值,而不是内存地址。

addl %eax,%eax               // eax = eax + eax

正确。 eax 的值在这里没有修改,所以这是函数的返回值。

movl %ebp,%esp               // eax = t
popl %ebp                    // pop a from stack
ret

此代码反转前两条指令的效果。这是一个标准的清理顺序,与a无关。

这个函数的重要部分可以概括为:

a1 = a << 4;   // = a * 16
a2 = a1 + a;   // = a * 17
a3 = a2 + a2;  // = a * 34
return a3;

【讨论】:

  • “你把它倒过来了。这会将 eax(它携带 a 的原始值)移动到堆栈中。” 不,这是 AT&T 语法,所以你得到了它向后......它将值从堆栈内存加载到eax,即真正将a加载到eax。 ... a 在 OP 的调用约定中在堆栈上发送,eax 在进入时可以包含几乎任何内容。
  • @Ped7g 啊,你是对的——更正了我的帖子。我在考虑 x86-64 调用约定。
【解决方案2】:

这是非常糟糕的未优化代码,因为您使用 -O0 编译(编译速度快,跳过大多数优化通道)。传统的堆栈框架设置/清理只是噪音。 arg 位于返回地址正上方的堆栈中,即函数入口处的 4(%esp)。 (另见How to remove "noise" from GCC/clang assembly output?

令人惊讶的是,编译器使用 3 条指令通过移位和加法进行乘法运算,而不是 imull $34, 4(%esp), %eax / ret,除非针对旧 CPU 进行调整。 2 条指令是现代 gcc 和 clang 及其默认调优的截止点。参见例如How to multiply a register by 37 using only 2 consecutive leal instructions in x86?

但是这个可以使用 LEA 用 2 条指令来完成(不包括 mov 复制寄存器);代码很臃肿,因为你编译时没有优化。 (或者您针对旧 CPU 进行了调优,可能出于某种原因避免使用 LEA。)

我想你一定为此使用了 gcc;禁用其他编译器的优化总是只使用 imul 乘以非 2 的幂。但是我在 Godbolt 编译器资源管理器上找不到 gcc 版本 + 选项,它可以准确地提供您的代码。我没有尝试所有可能的组合。 MSVC 19.10 -O2 使用与您的代码相同的算法,包括两次加载 a

使用 gcc5.5(它是最新的 gcc,不仅使用 imul,甚至使用 -O0)编译,我们得到类似您的代码的东西,但不完全一样。 (以不同的顺序进行相同的操作,并且不会从内存中加载两次a)。

# gcc5.5 -m32 -xc -O0 -fverbose-asm -Wall
func:
    pushl   %ebp  #
    movl    %esp, %ebp      #,            # make a stack frame

    movl    8(%ebp), %eax   # a, tmp89    # load a from the stack, first arg is at EBP+8

    addl    %eax, %eax      # tmp91          # a*2
    movl    %eax, %edx      # tmp90, tmp92
    sall    $4, %edx        #, tmp92         # a*2 << 4 = a*32
    addl    %edx, %eax      # tmp92, D.1807  # a*2 + a*32

    popl    %ebp    #                     # clean up the stack frame
    ret

the Godbolt compiler explorergcc5.5 -m32 -O3 -fverbose-asm 上使用相同的旧 GCC 版本编译优化,我们得到:

# gcc5.5 -m32 -O3.   Also clang7.0 -m32 -O3 emits the same code
func:
    movl    4(%esp), %eax   # a, a          # load a from the stack
    movl    %eax, %edx      # a, tmp93      # copy it to edx
    sall    $5, %edx        #, tmp93        # edx = a<<5 = a*32
    leal    (%edx,%eax,2), %eax             # eax = edx + eax*2 = a*32 + a*2 = a*34
    ret              # with a*34 in EAX, the return-value reg in this calling convention

使用 gcc 6.x 或更高版本,我们得到了这种高效的 asmimul-immediate 与内存源在现代 Intel CPU 上仅解码为单个微融合 uop,并进行整数乘法英特尔自 Core2 以来只有 3 个周期延迟,AMD 自 Ryzen 以来只有 3 个周期延迟。 (https://agner.org/optimize/)。

# gcc6/7/8 -m32 -O3     default tuning
func:
    imull   $34, 4(%esp), %eax    #, a, tmp89
    ret

但是对于 -mtune=pentium3,我们奇怪地没有获得 LEA。这看起来像是错过了优化。 LEA 在 Pentium 3 / Pentium-M 上有 1 个周期的延迟。

# gcc8.2 -O3 -mtune=pentium3 -m32 -xc -fverbose-asm -Wall
func:
    movl    4(%esp), %edx   # a, a
    movl    %edx, %eax      # a, tmp91
    sall    $4, %eax        #, tmp91     # a*16
    addl    %edx, %eax      # a, tmp92   # a*16 + a = a*17
    addl    %eax, %eax      # tmp93      # a*16 * 2 = a*34
    ret

这与您的代码相同,但使用 reg-reg mov 而不是从堆栈中重新加载将a 添加到移位结果中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-26
    • 1970-01-01
    • 2015-03-03
    • 2015-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多