从汇编代码设计C语言中的函数

问题描述 投票:0回答:3

我需要设计C语言的功能,以实现用机器代码编写的内容。我可以逐步执行组装操作,但是据说我的功能实现不正确。我很困惑。

这是该函数的反汇编代码。

(Hand transcribed from an image, typos are possible
 especially in the machine-code.  See revision history for the image)
0000000000000000 <ex3>:
   0:   b9 00 00 00 00       mov    0x0,%ecx
   5:   eb 1b                jmp    L2  // 22 <ex3+0x22>
   7:   48 63 c1     L1:     movslq %ecx,%rax
   a:   4c 8d 04 07          lea    (%rdi,%rax,1),%r8
   e:   45 0f b6 08          movzbl (%r8),%r9d
  12:   48 01 f0             add    %rsi,%rax
  15:   44 0f b6 10          movzbl (%rax),%r10d
  19:   45 88 10             mov    %r10b,(%r8)
  1c:   44 88 08             mov    %r9b,(%rax)
  1f:   83 c1 01             add    $0x1,%ecx
  22:   39 d1        L2:     cmp    %edx,%ecx
  24:   7c e1                jl     L1   // 7 <ex3+0x7>
  26:   f3 c3                repz retq

我的代码(未提供或确定函数的签名]:

#include <assert.h>

int
ex3(int rdi, int rsi,int edx, int r8,int r9 ) {
    int ecx = 0;
    int rax;
    if(ecx>edx){
         rax = ecx;
        r8 =rdi+rax;
        r9 =r8;
        rax =rsi;
        int r10=rax;
        r8=r10;
        rax =r9;
        ecx+=1;
    }
    return rax;
}

请识别出导致错误的原因。

c assembly x86-64 reverse-engineering
3个回答
2
投票

((编者注:这是一个部分答案,only解决了循环结构。它不涉及movzbl字节加载,也不包括某些变量是指针或类型宽度的事实。其他答案可以覆盖问题的其他部分。)


C支持goto,尽管经常不赞成使用它们,但它们在这里非常有用。使用它们使它与装配尽可能相似。这使您可以在开始引入更适当的控制流机制(如while循环)之前确保代码能够正常工作。所以我会做这样的事情:

    goto L2;
L1:
    rax = ecx;
    r8 =rdi+rax;
    r9 =r8;
    rax =rsi;
    int r10=rax;
    r8=r10;
    rax =r9;
    ecx+=1;
L2:
    if(edx<ecx) 
        goto L1;

您可以轻松地将上面的代码转换为:

while(edx<ecx) {
    rax = ecx;
    r8 =rdi+rax;
    r9 =r8;
    rax =rsi;
    int r10=rax;
    r8=r10;
    rax =r9;
    ecx+=1;
}

请注意,我尚未检查L1块中的代码以及之后的while块中的代码是否正确。 (编者注:它丢失了所有的内存访问)。但是您的跳错是错误的,现在已得到纠正。

需要注意的一件有趣的事情是,如果我们删除了goto L2语句,那么它等效于

do {
    // Your code
} while(edx<ecx);

一个while循环可以通过附加的goto编译为do-while循环。 (请参见Why are loops always compiled into "do...while" style (tail jump)?)。这很容易理解。

在汇编中,循环由在代码中向后跳转的gotos构成。您进行测试,然后决定是否要跳回去。因此,为了在第一次迭代之前进行测试,您需要首先跳至测试。 (编译器有时还会编译while循环,其顶部为if()break,底部为jmp。但仅在禁用优化的情况下才能看到。请参见While, Do While, For loops in Assembly Language (emu8086)

前跳通常是编译if语句的结果。

我也刚刚意识到,我现在有三种使用goto的好方法。前两个是打破嵌套循环并以相反的分配顺序释放资源。


0
投票

对于那些喜欢将.S格式用于GCC的人,我使用了:

ex3:
  mov $0x0, %ecx
  jmp lpe
  lps:
      movslq %ecx, %rax
      lea (%rdi, %rax, 1), %r8
      movzbl (%r8), %r9d
      add %rsi, %rax
      movzbl (%rax), %r10d
      mov %r10b, (%r8)
      mov %r9b, (%rax)
      add $0x1, %ecx
  lpe:
  cmp %edx, %ecx
  jl lps
repz retq


.data
.text
.global _main
_main:
    mov $0x111111111111, %rdi
    mov $0x222222222222, %rsi
    mov $0x5, %rdx
    mov $0x333333333333, %r8
    mov $0x444444444444, %r9
    call ex3
    xor %eax, %eax
    ret

然后您可以用gcc main.S -o main对其进行编译,然后运行objdump -x86-asm-syntax=intel -d main以intel格式查看它,或者在反编译器中运行生成的main可执行文件。.但是,让我们进行一些手动工作。

首先,我将AT&T语法转换为更常见的Intel语法。.::

ex3:
  mov ecx, 0
  jmp lpe
  lps:
      movsxd rax, ecx
      lea r8, [rdi + rax]
      movzx r9d, byte ptr [r8]
      add rax, rsi
      movzx r10d, byte ptr [rax]
      mov byte ptr [r8], r10b
      mov byte ptr [rax], r9b
      add ecx, 0x1
  lpe:
  cmp ecx, edx
  jl lps
rep ret

现在我可以清楚地看到,从lps(循环开始)到lpe(循环结束)是for循环。

如何?因为首先将计数器寄存器(ecx)设置为0,然后通过先执行ecx < edx再执行cmp ecx, edx(如果小于则跳转)检查jl。如果是,则运行代码。并将ecx加1(add ecx, 1)..如果不存在,则该块存在。

因此,它看起来像:for (int32_t ecx = 0; ecx < edx; ++ecx) ..(请注意edx是rdx的低32位)。

所以现在我们用以下知识翻译其余内容:

r10是64位寄存器。 r10d是高32位,r10b是低8位。r9是一个64位寄存器。适用与r10相同的逻辑。

所以我们可以代表一个寄存器,如下所示:

typedef union Register
{
    uint64_t reg;
    struct
    {
        uint32_t upper32;
        uint32_t lower32;
    };

    struct
    {
        uint16_t uupper16;
        uint16_t ulower16;
        uint16_t lupper16;
        uint16_t llower16;
    };

    struct
    {
        uint8_t uuupper8;
        uint8_t uulower8;

        uint8_t ulupper8;
        uint8_t ullower8;

        uint8_t luupper8;
        uint8_t lulower8;

        uint8_t llupper8;
        uint8_t lllower8;
    };
} Register;

哪个更好。您可以自己选择。现在我们可以开始查看说明了。movsxdmovslq将32位寄存器移动到带符号扩展名的64位寄存器中。

现在我们可以编写代码:

uint8_t* ex3(uint8_t* rdi, uint64_t rsi, int64_t rdx)
{
    uintptr_t rax = 0;
    for (int32_t ecx = 0; ecx < (rdx & 0xFFFFFFFF); ++ecx)
    {
        rax = ecx;
        uint8_t* r8 = rdi + rax;
        Register r9 = { .reg = *r8 }; //zero extend into the upper half of the register
        rax += rsi;

        Register r10 = { .reg = *(uint32_t*)rax }; //zero extend into the upper half of the register
        *r8 = r10.lllower8;
        *(uint8_t*)rax = r9.lllower8;
    }

    return rax;
}

希望我没有弄乱任何东西。。

我很确定是这样:交换两个内存区域:

void memswap(unsigned char *rdi, unsigned char *rsi, int edx) {
        int ecx;
        for (ecx = 0; ecx < edx; ecx++) {
                unsigned char r9 = rdi[ecx];
                unsigned char r10 = rsi[ecx];
                rdi[ecx] = r10;
                rsi[ecx] = r9;
        }
}

0
投票

我很确定是这样:交换两个内存区域:

void memswap(unsigned char *rdi, unsigned char *rsi, int edx) {
        int ecx;
        for (ecx = 0; ecx < edx; ecx++) {
                unsigned char r9 = rdi[ecx];
                unsigned char r10 = rsi[ecx];
                rdi[ecx] = r10;
                rsi[ecx] = r9;
        }
}
© www.soinside.com 2019 - 2024. All rights reserved.