繁体   English   中英

根据汇编代码在 C 中设计一个 function

[英]Design a function in C from assembly code

我需要用 C 语言设计一个 function 来实现机器码中写的内容。 我分步进行组装操作,但据说我的 function 执行错误。 我很困惑。

这是function的反汇编代码。

(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

我的代码( function的签名没有给出或结算):

#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;
}

如果您发现任何错误,请解释导致错误的原因。

(编者注:这是解决循环结构的部分答案。它不涵盖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 块中的代码是否正确。 (编者注:它缺少所有 memory 访问)。 但是你的跳跃是错误的,现在已经纠正了。

您可以从这里(再次假设这是正确的)开始尝试查看模式。 似乎ecx被用作某种索引变量。 并且变量rax可以在开始时被替换。 我们可以做一些其他类似的改变。这给了我们:

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

在这里,显然有些事情有点不确定。 while 条件是edx<ii每次迭代都会递增而不是递减。 这是一个很好的迹象,表明有问题。 我在组装方面不够熟练,无法弄清楚,但至少这是您可以使用的方法。 一步一步来。

add $0x1,%ecx是用于将ecx加 1 的 AT&T 语法。根据该站点使用 Intel 语法,结果存储在第一个操作数中。 在 AT&T 语法中,这是最后一个操作数。

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

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

可以将 while 循环编译为带有附加 goto 的 do-while 循环。 (请参阅为什么循环总是编译为“do...while”样式(尾跳)? )。 这很容易理解。

在汇编中,循环是由在代码中向后跳转的 goto 构成的。 您进行测试,然后决定是否要跳回。 所以为了在第一次迭代之前进行测试,需要先跳转到测试。 (编译器有时也会编译 while 循环,在顶部使用if()break并在底部使用jmp 。但仅在禁用优化的情况下。请参见汇编语言中的 While、Do While、For 循环 (emu8086)

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

我也刚刚意识到我现在有了三种使用 goto 的好方法。 前两个是打破嵌套循环并以相反的分配顺序释放资源。 现在是第三个,当你对装配进行逆向工程时。

我很确定是这样的:交换 memory 的两个区域:

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;
        }
}

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

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。然后它通过执行cmp ecx, edx后跟jl (如果小于则跳转)来检查ecx < edx 。如果是,它运行代码并递增ecx by 1 ( add ecx, 1 ).. 如果没有,它存在块..

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

因此,现在我们根据以下知识翻译 rest:

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, int32_t edx)
{
    uintptr_t rax = 0;
    for (int32_t ecx = 0; ecx < edx; ++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 = *(uint8_t*)rax }; //zero extend into the upper half of the register
        *r8 = r10.lllower8;
        *(uint8_t*)rax = r9.lllower8;
    }

    return rax;
}

希望我没有搞砸什么。。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM