我如何导航和描述这个函数试图从给定的汇编代码中实现什么?

How can I navigate and describe what this function is trying to achieve from the given assembly code?

根据我的教科书,我在 x86-64 汇编中得到了以下函数:

foo:                             # line 1
       movl [=10=], %eax             # line 2
       movl [=10=], %r8d             # line 3
       jmp .L2                   # line 4
.L3:                             # line 5
       addl , %eax             # line 6
.L2:                             # line 7
       cmpl %esi, %eax           # line 8
       jge .L5                   # line 9
       movslq %eax, %rcx         # line 10
       cmpl %edx, (%rdi,%rcx,4)  # line 11
       jne .L3                   # line 12
       addl , %r8d             # line 13 
       jmp .L3                   # line 14
.L5:                             # line 15
       movl %r8d, %eax           # line 16
       ret                       # line 17

到目前为止,我想我已经能够用适当的 C 类型找出函数的 C 签名。由于大小说明符都是 l,我假设它返回一个 int 并且该函数的所有参数也是 int。这是我拥有的:

int foo(int arg1, int arg2, int arg3)

进一步观察,该函数还包含一个for循环。使用与所用寄存器名称相对应的变量名称(例如,使用 eax 表示 %eax),这是我认为的循环结构:

for (eax = 0; eax < esi; eax++)

但是,我很难描述这个函数实际要完成的任务。跳转指令和 movslq %eax, %rcx 让我感到困惑。任何人都可以帮助我浏览此功能的程序集并帮助我了解它试图实现的目标吗?我对汇编比较陌生,我还没有完全习惯阅读它。任何帮助或建议将不胜感激,以增加我对汇编的理解。

你看到的movslq实际上是MOVSX指令,在AT&T语法中叫做MOVSLQ(是的,傻,我知道)。您使用后缀 l(long = 4 字节)和 q(quadruple = 8 字节)来指示两个操作数的大小。这是一个简单的动作,符号从较小的寄存器扩展到较大的寄存器(在您的例子中是 EAX 到 RCX)。

跳转有点复杂,但你可以像这样将程序集翻译成伪 C 代码:

foo:                             # 
       movl [=10=], %eax             # eax = 0
       movl [=10=], %r8d             # r8d = 0
       jmp .L2                   # goto L2
.L3:                             # 
       addl , %eax             # eax++
.L2:                             # 
       cmpl %esi, %eax           # if (eax >= esi)
       jge .L5                   #     goto L5
       movslq %eax, %rcx         # rcx = eax
       cmpl %edx, (%rdi,%rcx,4)  # if (rdi[rcx] != edx)
       jne .L3                   #     goto L3
       addl , %r8d             # r8d++
       jmp .L3                   # goto L3
.L5:                             # 
       movl %r8d, %eax           # 
       ret                       # return r8d

这里的要点是:

  1. x86-64 System V 调用约定使用 RDI、RSI、RDX、[...] 作为参数,RAX 作为 return 值。
  2. 我们可以看到RDI被原样使用,而其他两个参数被当作4字节参数:我们只看到ESI和EDX出现
  3. 要return编辑的实际值位于 R8D 中,也是 4 个字节。
  4. 循环变量为EAX,开头的cmpl %esi, %eax; jge .L5为循环的控制条件,即ESI保存的是数组的总长度。此外,JGE 指令是 signed (see this quick reference),这意味着 EAX 和 ESI 被视为带符号的 4 字节整数(即 C int).
  5. 比较 cmpl %edx, (%rdi,%rcx,4) 让我们明白 RDI 被用作 4 字节元素数组的基地址(例如 int),因为这个操作取消引用 rdi + rcx * 4 到得到一个“长”(AT&T 术语中的 4 个字节,l 后缀)。
  6. 该比较正在检查 EDX(第二个参数)是否等于 RDI 引用的数组中位置 RCX 处的元素。我们可以看到 RCX 来自 EAX,所以这是检查当前索引处的元素。
  7. 如果比较失败 (jne),我们不会递增 R8D。否则,如果成功,我们增加 R8D。这是倒逻辑,但我们可以简单地否定它来理解只有当当前索引处的元素等于 EDX(第三个参数)时 R8D 才会增加。

考虑到所有这些,我们可以继续简化代码:

int func(int *rdi, int esi, int edx) {
    int eax = 0;
    int r8d = 0;

    for ( ; ; eax++) {
        if (eax >= esi)
            return r8d;

        int rcx = eax;

        if (rdi[rcx] != edx)
            continue;

        r8d++;
    }

    return r8d;
}

此时应该很清楚该函数的作用:它扫描一个整数数组,计算与给定值匹配的元素的数量。

我们可以更简洁地重写为:

int func(int *rdi, int esi, int edx) {
    int r8d = 0;
    int eax;

    for (eax = 0; eax < esi; eax++) {
        if (rdi[eax] == edx)
            r8d++;
    }

    return r8d;
}