理解和分析汇编代码

3

有人能帮我理解这段汇编代码吗?我完全不懂汇编语言,根本无法弄清楚…… 以下汇编代码应该生成此函数:

func(int a) { return a * 34 }

//注释是我的想法,如果我错了,请纠正我。

//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

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


3
首两行与 a 无关,只是设置了栈帧。8(%ebp) 表示的是 a - Jester
2个回答

7
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)

不,你误解了这个。这会将栈上的值添加到eax中,在8(ebp)处 -- 即a的原始值 -- 的内存地址不会被改变。加法应用于值,而非内存地址。
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;

1
你搞反了。这个操作是将eax(它携带着a的原始值)移动到堆栈中。不,这是AT&T语法,所以你搞反了...它从堆栈内存加载值到eax中,也就是真正地将a加载到eax中。...在OP的调用约定中,a被发送到堆栈上,而eax在进入时可以包含任何内容。 - Ped7g
@Ped7g 哦,你说得对 - 我已经更正了我的帖子。我在想x86-64调用约定。 - user149341

2
这是一段不够优化的代码,因为你使用了-O0(快速编译,跳过大部分优化步骤)。传统的堆栈帧设置/清理只是噪音。参数在返回地址的正上方处于堆栈上,即在函数进入时在4(%esp)位置。(另请参见如何从GCC / clang汇编输出中去除“噪音”?
令人惊讶的是,编译器使用3条指令通过移位和加法来进行乘法运算,而不是使用imull $34, 4(%esp), %eax / ret,除非针对旧CPU进行调整。2条指令是现代gcc和clang默认调优的截止值。例如,请参见如何使用两条连续的leal指令将寄存器乘以37? 但是,可以使用LEA的2条指令来实现这个目标(不包括用于复制寄存器的mov);代码膨胀是因为你没有进行优化编译(或者你调优的是旧的CPU,在那里可能有一些原因避免使用LEA)。
我认为您一定使用了gcc进行编译;使用其他编译器禁用优化时,总是使用imul来乘以非2次幂。但是我在Godbolt编译器资源管理器中找不到完全给出您的代码的gcc版本+选项。我没有尝试每种可能的组合。MSVC 19.10 -O2使用与您的代码相同的算法,包括两次加载a
使用gcc5.5进行编译(这是最新的gcc,即使在-O0下,也不仅仅使用imul),我们得到类似于您的代码的东西,但并不完全相同。(以不同的顺序执行相同的操作,并且不会两次从内存加载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

Godbolt编译器浏览器

# 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或更高版本,我们可以获得这个高效的汇编代码:带有内存源的imul-立即数只解码为现代英特尔CPU上的单个微融合uop,并且自Core2以来,整数乘法在英特尔和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指令。看起来这是一次未成功的优化。在Pentium 3 / Pentium-M上,LEA指令的延迟只有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 添加到移位结果中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接