诸如diffkdiff3或什至更复杂的文本区分工具通常如何无法以文本形式突出显示两个反汇编之间的差异,特别是两个相关的二进制可执行文件,例如同一程序的不同版本?

这是吉尔斯在评论中问的问题:


为什么拆卸时的diff / meld / kdiff / ...不令人满意?


我认为这个问题值得回答,所以我给出了一个问答式的问题,因为出于某些奇怪的原因,它不适合包含600个字符;)

请不要不过,请不要错过Rolf的答案!

#1 楼

索引(已缩短)


Gentle Intro-二进制可执行代码,它看起来如何?
为什么比较二进制可执行代码是一项艰巨的任务?
结论<解决方案
TL; DR

TL; DWTR(时间太长,不想阅读):跳至本部分,为什么比较二进制可执行代码是一项艰巨的任务?如果您对组装和拆卸的基础知识感到满意。或者,跳到该答案的底部(TL; DR)。

Gentro Intro-二进制可执行代码,它的外观如何?

二进制可执行代码是供计算机使用的阅读,这就是为什么它通常被称为机器代码的原因。这意味着它是二进制“数据”,通常用肉眼用十六进制数表示。存在用于此任务的名为“十六进制编辑器”的工具类别。

使用HTE作为屏幕截图的常见外观如下: >之所以如此方便,是因为每个十六进制数字正好代表一个半字节,即4位。因此,可以使用两个十六进制数字来表示单个8位字节(最常见的字节类型)。然后以每行16字节的倍数显示它们还有一个额外的优点,即可以更轻松地读取十六进制偏移量(在上面的屏幕快照中,十六进制字节前面为8位十六进制数字),因为十进制160x10。十六进制数最常见的表示法是:


前缀0x:例如0x10(C和相关语言)
前缀$:例如(帕斯卡,德尔福)
后缀h:例如10h(汇编语言)

旁注:代码和数据之间的界线很细,反汇编程序有时很难将二进制文件中的字节标识为一个或另一个,尽管可以使用启发式方法在识别过程中提供帮助。

组装

除了十六进制数字的“原始”形式外,还有人类可读的表示形式,即汇编语言。这是二进制指令(或操作码)的一种助记符形式,通常以很小的抽象表示1:1。值得注意的例外是宏汇编程序(例如Microsoft的MASM),它们提供了更高级别的抽象以方便使用。

旁注:提取汇编语言代码(也称为“汇编代码”或“汇编程序代码”)被称为汇编程序。

取决于处理器的类型和确切的体系结构,存在各种形式的汇编语言。对于此问题,我们将继续使用IA-32体系结构-也称为x86(32位,x86),因为其起源是8088和8086处理器,后续处理器(CPU)型号编号为80x86,其中x是一位数字。从80586 Intel开始,他们在引入Pentium时就已经采用了这种命名方案。 (SPARC,MIPS,PPC),而一个或另一个的狂热者却一次或一次宣称他们偏爱的架构取得了胜利,尽管至今在微代码级别,甚至有人甚至认为CISC架构是RISC“内部”。也就是说,x86是CISC架构。

让我们简要了解一下RISC和CISC的外观,让我们看一下一些基本的x86和MIPS指令:

 x86          |    MIPS              | Meaning
-----------------------------------------------
 mov          | lb, lw, sb, move     | copy/"move" value from/to register/memory location
 jmp, jz, jnz | j/b, beq, beqz, bgez | jump unconditionally or on condition
 call         | jal                  | call other routine / jump and link


乍看起来(与mov一致)的希望是显而易见的事实是,在MIPS中,您所拥有的非常基本的指令数量比x86(这是CISC与RISC范例的要旨)要多得多。另一件事是,我们看到MIPS如何使用jb作为跳转和分支的助记符前缀(区别通常是这些“跳转”可以覆盖的距离),而x86也使用j,如jmp(无条件跳转),jnz(如果不为零则跳转[-flag set]),但还有一个专用的call操作码,据我所知MIPS没有它-最接近的近似值可能是jal(跳转和链接),它也将程序计数器存储在寄存器中,而不是但是x86的call会将其存储在堆栈中。

在CISC中,您可以用一条指令执行相对复杂的操作,而在RISC中,您通常需要多条指令来表示同一件事。实际上,用于RISC体系结构的汇编程序往往具有所谓的伪指令,该伪指令结合了常用的指令组合,但是在转换阶段会转换为单独的RISC指令。

ror(向右旋转)指令。在x86(CISC)上,它本身就是带有ror助记符的操作码。在MIPS(RISC)上,如果汇编器提供了伪指令,则它是伪指令,它的转换方式如下(将寄存器$t2中的值旋转一位,然后再次将结果存储在$t2中):

ror $t2, $t2, 1    -->   sll  , , 31  (bit-shift  left by 31, store in )
                         srl , , 1   (bit-shift  right by 1, store in )
                         or  , ,   (bit-wise or  and )


本示例摘自本书,该书从此处引用而来(MIPS资源)第370页。

但是,我们不会进一步研究汇编语言的基础知识,而是专注于回答问题。我认为,除了最基本的事实以外,我们不需要了解太多,而是要理解为什么简单甚至复杂的diff工具无法显示二进制可执行文件的差异。

编译器和汇编语言

编译器(编译器)将通常由人或机器编写的代码构造转换为机器代码[*]。通常的翻译是将人类可读的源代码转换成中间形式,然后对其进行优化,并在优化后转换为汇编语言,而汇编语言又将其转换为机器代码。这在上面的Wikipedia文章中得到了很好的展示,下面是我正在复制的图表:最后,即使您自己可能永远也看不到实际的汇编语言说明。如果要获取它,可以使用:


对于GCC:gcc -S ...(AT&T语法)或gcc -masm=intel -S ...(英特尔语法)
对于Microsoft Visual C ++:cl.exe /Fa ...
*

[*]这不是全部,因为有些编译器会创建一个中间字节代码,然后将其进一步翻译为运行或解释的CPU固有的机器代码。飞。但是对于此答案的范围,我们将编译器视为通过多个处理阶段将人类可读的高级语言源代码转换为机器代码的实体,其中一个涉及汇编代码。

Intel与AT&T语法对比

对于x86,存在两个竞争的语法变体。 AT&T语法在* nix世界以及GASM,Windows世界的Intel和大多数反汇编程序和汇编程序中受到青睐。考虑这个简单的C程序:

#include <stdio.h>

int main()
{
    printf("Hello world!\n");
    return 0;
}


...以及分别采用AT&T(gcc -S hello.c或显式gcc -masm=att -S hello.c)和Intel(gcc -masm=intel -S hello.c)语法的翻译:
 AT&T                        |  Intel
------------------------------------------------------------------------
.LC0:                        | .LC0:
    .string "Hello world!"   |     .string "Hello world!"
    .text                    |     .text
.globl main                  | .globl main
    .type   main, @function  |     .type   main, @function
main:                        | main:
    pushl   %ebp             |     push    ebp
    movl    %esp, %ebp       |     mov ebp, esp
    andl    $-16, %esp       |     and esp, -16
    subl    , %esp        |     sub esp, 16
    movl    $.LC0, (%esp)    |     mov DWORD PTR [esp], OFFSET FLAT:.LC0
    call    puts             |     call    puts
    movl    
mov DWORD PTR [esp], OFFSET FLAT:.LC0
, %eax | mov eax, 0 leave | leave ret | ret


您会注意到语法的不同。 AT&T语法中的寄存器用%表示,文字值用$表示,源寄存器和目标寄存器的位置与Intel语法相反。此外,某些助记符也有所不同(movl而不是mov)。在Intel语法中,操作数的大小有助于推断预期的操作-在寄存器的情况下,分别使用EAX,AX和AL / AH进行了明确表示,分别表示DWORD(32位),WORD(16位)和BYTE(8位)大小。但是,以下行:

#include <stdio.h>

int syntax_help(int argc)
{
        return 20 + argc;
}

int main(int argc, char **argv)
{
        if (argc < 3)
                return syntax_help(argc);
        else if (argc == 3)
                return 42;
        // else ...
        return 0;
}


巧妙地显示了如何明确说明内存位置的大小才能正确处理。 AT&T movl助记符隐含此含义,因为其固有的含义是“ move long”(此处为32bit),因此无需提及我们正在访问的DWORD不同于l中的movl

请注意:为简洁起见,我在生成的汇编代码的顶部和底部修剪了一些无关的部分。

MIPS资源

对于倾斜的阅读器,我建议您获得一份优秀,尽管价格昂贵:


“汇编语言编程简介”,第二版,作者Sivarama P. Dandamudi,Springer 2004/2005(ISBN 978-0-387-20636-3 )

如果要尝试使用MIPS,可以获取SPIM,查阅其文档或只是使用搜索引擎来查找有用的信息,例如本快速教程。

x86资源


以上在MIPS资源中提到的书还讨论了x86汇编
Randall Hyde的“汇编艺术”书

Iczelion的旧网站再次使用搜索引擎进行搜索。查找更多信息或咨询您喜欢的汇编程序(例如NASM)的文档。

反汇编

将二进制机器语言转换回助记符表示的过程,通常是1: 1,称为拆装。该过程的结果通常也称为反汇编。

用于该过程的工具称为反汇编器。

因为这主要是1:1的过程就像反向(汇编为机器代码)一样,无需进行更多详细说明。手写或编译器生成的汇编与反汇编所得的二进制代码之间有一个很大的区别,通过比较反汇编器和编译器的输出,我们会发现更好。

因此,事不宜迟,让我们看一个实际的例子,说明为什么很难进行区分。

为什么比较二进制可执行代码是一项艰巨的任务?

注意:对于该答案的其余部分,我们将使用Intel语法作为汇编代码。为了简洁起见,我们还将删除GCC输出的一些冗余部分。

示例程序-第一次迭代

C版本

在我们的第一次迭代中我们有以下C代码(我将其命名为ptest1.c):

.globl syntax_help
        .type   syntax_help, @function
syntax_help:
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR [ebp+8]
        add     eax, 20
        pop     ebp
        ret
        .size   syntax_help, .-syntax_help
.globl main
        .type   main, @function
main:
        push    ebp
        mov     ebp, esp
        sub     esp, 4
        cmp     DWORD PTR [ebp+8], 2
        jg      .L4
        mov     eax, DWORD PTR [ebp+8]
        mov     DWORD PTR [esp], eax
        call    syntax_help
        jmp     .L5
.L4:
        cmp     DWORD PTR [ebp+8], 3
        jne     .L6
        mov     eax, 42
        jmp     .L5
.L6:
        mov     eax, 0
.L5:
        leave
        ret


汇编语言版本(具有GCC,Intel语法)

。 。使用gcc -O0 -masm=intel -S -o ptest1.asm ptest1.c将其编译为汇编程序可以得到:

#include <stdio.h>

int syntax_help(int argc)
{
        switch (argc)
        {
        case 0:
                return -1;
        case 1:
                return 23;
        default:
                return 20 + argc;
        }
}

int main(int argc, char **argv)
{
        if (argc < 5)
                return syntax_help(argc);
        else if (argc == 5)
                return 42;
        // else ...
        return 0;
}


示例程序-第二次迭代

现在让我们稍微修改一下程序,然后再次组装,只看外观。

C版本

.globl syntax_help
        .type   syntax_help, @function
syntax_help:
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR [ebp+8]
        test    eax, eax
        je      .L3
        cmp     eax, 1
        je      .L4
        jmp     .L7
.L3:
        mov     eax, -1
        jmp     .L5
.L4:
        mov     eax, 23
        jmp     .L5
.L7:
        mov     eax, DWORD PTR [ebp+8]
        add     eax, 20
.L5:
        pop     ebp
        ret
        .size   syntax_help, .-syntax_help
.globl main
        .type   main, @function
main:
        push    ebp
        mov     ebp, esp
        sub     esp, 4
        cmp     DWORD PTR [ebp+8], 4
        jg      .L9
        mov     eax, DWORD PTR [ebp+8]
        mov     DWORD PTR [esp], eax
        call    syntax_help
        jmp     .L10
.L9:
        cmp     DWORD PTR [ebp+8], 5
        jne     .L11
        mov     eax, 42
        jmp     .L10
.L11:
        mov     eax, 0
.L10:
        leave
        ret


如您所见,3中的两个main实例更改为5,我们对syntax_help中的“逻辑”进行了一些修改。显然,这是一个人为的示例,但这才是重点。

汇编语言版本(与上述选项相同)

.globl syntax_help
    .type   syntax_help, @function
syntax_help:
    push    ebp
    mov ebp, esp
    mov eax, DWORD PTR [ebp+8]
    test    eax, eax
    je  .zero_args
    cmp eax, 1
    je  .one_arg
    jmp .return_20plus
.zero_args:
    mov eax, -1
    jmp .exit_help
.one_arg:
    mov eax, 23
    jmp .exit_help
.return_20plus:
    mov eax, DWORD PTR [ebp+8]
    add eax, 20
.exit_help:
    pop ebp
    ret
    .size   syntax_help, .-syntax_help
.globl main
    .type   main, @function
main:
    push    ebp
    mov ebp, esp
    sub esp, 4
    cmp DWORD PTR [ebp+8], 4
    jg  .return_42
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [esp], eax
    call    syntax_help
    jmp .exit
.return_42:
    cmp DWORD PTR [ebp+8], 5
    jne .return_0
    mov eax, 42
    jmp .exit
.return_0:
    mov eax, 0
.exit:
    leave
    ret

一口现在,除了“优化”方面之外,让我们深入研究这一点与潜在的人工编写的组件之间的区别。这是一个人工编写的版本,如下所示:

.globl syntax_help
    .type   syntax_help, @function
syntax_help:
    push    ebp
    mov ebp, esp
    mov eax, DWORD PTR [ebp+8]
    add eax, 20
    pop ebp
    ret
    .size   syntax_help, .-syntax_help
.globl main
    .type   main, @function
main:
    push    ebp
    mov ebp, esp
    sub esp, 4
    cmp DWORD PTR [ebp+8], 2
    jg  .return_42
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [esp], eax
    call    syntax_help
    jmp .exit
.return_42:
    cmp DWORD PTR [ebp+8], 3
    jne .return_0
    mov eax, 42
    jmp .exit
.return_0:
    mov eax, 0
.exit:
    leave
    ret


曾经编写汇编代码的人都将不可避免地注意到我没有声明变量(dbdwdd ) 这里。这将是正常的做法,但是,当然,在这里,我仅表明我们人类倾向于将符号名称赋予代码位置(和变量)。如果您是手工编写汇编,则看起来仍然会有所不同,我只是调整了代码,使其看起来更像人类可以编写的内容(即,它不是完美的,而且肯定不是“手动优化”的)。编译器将顽固而有效地在某种带字母的前缀上附加一个数字,并以此来完成。让我们还使用相同的名称创建一个可能的人为版本的第一次迭代:

1c1
<       .file   "ptest1.c"
---
>       .file   "ptest2.c"
9a10,22
>       test    eax, eax
>       je      .L3
>       cmp     eax, 1
>       je      .L4
>       jmp     .L7
> .L3:
>       mov     eax, -1
>       jmp     .L5
> .L4:
>       mov     eax, 23
>       jmp     .L5
> .L7:
>       mov     eax, DWORD PTR [ebp+8]
10a24
> .L5:
20,21c34,35
<       cmp     DWORD PTR [ebp+8], 2
<       jg      .L4
---
>       cmp     DWORD PTR [ebp+8], 4
>       jg      .L9
25,28c39,42
<       jmp     .L5
< .L4:
<       cmp     DWORD PTR [ebp+8], 3
<       jne     .L6
---
>       jmp     .L10
> .L9:
>       cmp     DWORD PTR [ebp+8], 5
>       jne     .L11
30,31c44,45
<       jmp     .L5
< .L6:
---
>       jmp     .L10
> .L11:
33c47
< .L5:
---
> .L10:


比较编译器生成的汇编代码

这是diff ptest1.asm ptest2.asm(由编译器生成的形式)的输出:

6a7,19
>       test    eax, eax
>       je      .zero_args
>       cmp     eax, 1
>       je      .one_arg
>       jmp     .return_20plus
> .zero_args:
>       mov     eax, -1
>       jmp     .exit_help
> .one_arg:
>       mov     eax, 23
>       jmp     .exit_help
> .return_20plus:
>       mov     eax, DWORD PTR [ebp+8]
7a21
> .exit_help:
17c31
<       cmp     DWORD PTR [ebp+8], 2
---
>       cmp     DWORD PTR [ebp+8], 4
24c38
<       cmp     DWORD PTR [ebp+8], 3
---
>       cmp     DWORD PTR [ebp+8], 5


并不能完全帮助理解这些差异,是吗?

WinMerge提供更直观的结果。混乱随之而来...



NB:我决定不修改全高屏幕截图,而是注意左窗格,该窗格突出显示了差异(黄色)和缺失块(灰色)和移动的块(棕色... ish)。

比较“人工编写”的汇编代码

这是diff ptest1.asm-human ptest2.asm-human(“人工编写”形式):

objdump -M intel -d ptest1.stripped|grep '^ 80'|cut -f 2- -d ':'|sed 's/^\s*//g'
objdump -M intel -d ptest2.stripped|grep '^ 80'|cut -f 2- -d ':'|sed 's/^\s*//g'


哇,这几乎是可读的。使用colordiff很有用。

WinMerge中的各个视觉比较看起来完全可读:



插曲-基本块

反汇编程序只能在某种程度上具有智能性,因为它是一个程序。即使是撰写本文时最先进的反汇编程序的IDA Pro也无法正确猜测所有内容-例如区分代码或数据时。但是,更复杂的工具在这方面做得很好。并且IDA将I添加为交互式。

反汇编程序遇到的一件事是汇编程序员称为标签和(子)例程。

标签,尽管它们存在于C和C中。 (正确地)与goto一起被皱了皱眉,它们也存在于高级语言中,但是倾向于涵盖一些不同的概念。也许最接近汇编语言概念的是BASIC过去的美好时光。但是,将C编译为汇编代码时,每个条件都会转换为有条件或无条件跳转(上述编译器生成的代码中的jmpjejgjne)。跳转目标称为标签。跳转是代码有条件或无条件分支的地方。

与例程最接近的对应概念是C中的函数或Pascal中的procedure / function或BASIC中的sub。 >
call外,两个分支指令之间的每个代码块或多或少都称为基本块。在IDA Pro中,这可以在图形视图中清晰显示(可以通过默认空间在平面视图中切换):



通过箭头链接的每个块主要的IDA视图将是一个基本块。

再一次,为什么比较二进制可执行代码是一项艰巨的任务?

到现在为止,您应该已经有了一个模糊的主意,这使比较变得困难,但是让我们多加努力。让我们从比较由编译器生成和“人工编写”的汇编代码到进行实际的反汇编。

像以前一样,我们将坚持它的要旨。

由编译器生成对比反汇编

但是要提一下,在反汇编中,链接器对其进行了处理后,您会得到结果。以前由编译器生成的程序集仅包含我们在示例程序中编写的代码。

只是为了给您一个想法,我使用IDA生成了.asm文件,将其剥离为所有内容,而没有注释和空行,最后仍然是362行,而原始编译器生成的程序集中有52行,其中包括链接程序使用的元数据。这种巨大的差异当然可以归因于链接器添加了初始化可执行文件所需的各种代码。用更少的词:编译器(或更确切地说,它的链接器)添加的样板代码。

为了进行比较,我将完全省略该样板代码,尽管显然这只会增加复杂性比较二进制可执行代码时,将遇到一个diff工具。这意味着诸如ptest2.cmain之类的名称将不再存在。取而代之的是IDA Pro之类的反汇编程序通常会在偏移量后命名例程(例如syntax_help)。它也适用于标签(即命名sub_80483DBloc_80483F4)。当然,逆向工程师可以自由地将这些名称更改为自己更易于理解/识别的名称。但是默认名称仍取决于偏移量。

实际上,反汇编程序将很难识别locret_something函数,因为从可执行文件的入口点开始查看时,上述样板代码往往会先于其出现。这是IDA向您显示的,如果以前没有符号可用于已编译的main(即,运行ptest2.c):



现在让我们来看一下已编译的入口(和剥离的)strip -s ...



您注意到区别了吗?这很微妙,但让我为您并排放置:



是的,突出显示的行...偏移量不同。这是什么意思?

好吧,这意味着IDA Pro分配给例程以及标签(即基本块)的符号名称将根据文件中这些实体的偏移量而有所不同。

这确实与我们以前在编译器生成的汇编代码和带编号的标签名称中遇到的情况非常相似。

使用更简单的反汇编程序

让我们比较一个更简单的反汇编程序在文本中创建的相关代码段。

使用ptest1.c,然后除去前导的偏移量和空格,我们在WinMerge中的相关部分获得了此代码:



完整命令是:

q4312078q

结论

这意味着文本差异工具诸如objdump -M intel -d ...diffkdiff3等许多其他代码将很难比较拆卸,除非反向工程师花时间将所有例程和标签重命名为不基于偏移量的东西。

事实上,这变得面对文本形式的拆卸时,这几乎是无法克服的任务。 IDA Pro保留的内部形式更适合拆卸。

在文本形式中,每个更改的偏移量-都会有很多偏移量-会引起您的注意,因为这与文本的不同有所不同。

解决方案

知道问题所在,我们该怎么办?

基本问题解决了当前的问题。诸如DarunGrim(FLOSS),patchdiff2(FLOSS)和Bindiff(商业)之类的工具使用IDA有关基本块的知识来构建图形。这些图可用于识别相似和不同的块。使用图形形式的抽象,可视化效果可以叠加在IDA内的各个视图上,也可以提供专门的视图。您正在从中剥离很多上下文信息,IDA会在数据库中为您保留这些信息。而是从IDA已经拥有的信息中提取并使用它。插件和脚本使您可以进入IDA数据库的胆量,并提取其中的任何珍宝,从而以文本永远无法改变的方式来理解基本块。

工具

有关能够解决该任务的工具的列表,请参阅引发该问题的问题的答案:


如何在组装时比较两个x86二进制文件代码级别?

进一步阅读


请务必阅读下面的Rolf回答!

BinDiff的工作原理,由newgre回答
/>http://www.darungrim.org/演示文稿
http://www.darungrim.org/Researches

TL; DR

文本差异不足以处理文本反汇编的原因是,文本表示会丢弃反汇编程序在反汇编过程中收集的有价值的信息。反汇编程序还使用偏移量来命名代码位置和变量-对程序的更改以及随后的重新编译实际上将更改所有偏移量,因此会在文本表示形式中产生大量噪音。文本的不同会指出每一个,因此无法从反向工程师的角度找到相关的更改。

评论


我认为堆栈交换需要为您制作一个小说家徽章。辛苦了

– amccormack
13年4月22日在22:55

现在,这就是答案!

–达斯塔
13年4月23日在12:14

已收藏。可能会打印出来。

– jyz
15年6月5日在18:07

#2 楼

由于对源代码进行较小的更改会导致对已编译二进制文件进行较大的更改,因此大多数问题都将发挥作用。实际上,对源代码所做的任何更改仍然不会导致二进制文件的不同。

如果您想比较二进制文件,编译器优化会毁掉您的一天。最坏的情况是,如果您有两个二进制文件使用不同的编译器,不同版本的编译器或相同版本的编译器在不同的优化设置下进行编译。

我想到了一些示例:


内联。这种优化实际上可以完全删除功能,并且可以更改优化功能的控制流程图。
指令调度对给定基本块内的指令进行重新排序,以最大程度地减少流水线停顿。这对UNIX差异样式的工具造成了严重破坏。
循环不变的代码运动。这种优化实际上可以更改函数中基本块的数量!在不同的优化级别上编译的同一函数可以具有不同的控制流签名。
过程内寄存器分配。假设通过在引用该函数中已定义的某些变量的位置添加if语句来更改函数。使用变量的行为再次修改了函数变量的定义使用链。现在,当编译器为给定的函数生成低级代码时,它使用use-def信息在每个点上确定哪些变量应该在堆栈上,哪些应该放在寄存器中。这是过程内寄存器分配。因此,可能会发现,仅添加一行代码就会导致变量保存在不同的寄存器中,和/或保存在堆栈中,而不是保存在寄存器中(反之亦然),这显然会影响编译后的代码的外观。
诸如“过程间寄存器分配”(IRA),过程间公共子表达式消除(ICSE)等过程间优化极大地影响了已编译二进制文件的布局,并且它们也对源代码中的细微变化敏感。例如,IRA将为不需要符合标准调用约定的功能制造新颖的调用约定。因为它们不是从其包含的模块或库中导出的,并且永远不会通过函数指针进行引用。 ICSE可以从给定功能中删除部分代码。
配置文件引导的优化(PGO)。在这种优化下,编译器首先生成带有额外代码的二进制代码,该代码可计算有关程序运行时行为的统计信息。然后,程序员使检测到的代码经受“典型的工作量”并计算统计信息。然后,程序员重新编译程序,将这些统计信息提供给编译器,并告诉其通过PGO生成代码。然后,编译器通过按每个函数执行的频率,最常见的函数路径等顺序对代码进行排序,从而极大地改变了二进制代码的布局。不同的训练集将产生不同的统计信息,从而产生截然不同的可执行文件。 />
这不是详尽的清单。许多其他优化会困扰您。主要原因是由于编译器优化,所以UNIX差异样式工具在二进制比较空间中几乎没有实用性。

#3 楼

我要做的一件事是读取机器代码,然后将其转换回IR伪操作码,而没有任何寻址地址值,然后在对每个伪IR二进制文件使用这种缩减方法之后,执行这两个伪IR二进制文件之间的区别。

评论


(大致)相当于retdec等工具正在尝试的工具。是的,如今这是可行的(不过现在来看我问答的时间戳)。但是问题实际上是从何而来。还是对我+1,因为这很公平。顺便说一句,通过提供更多详细信息,您的答案可能会有所收获;)

– 0xC0000022L♦
5月11日在16:34



是的但是通常我们会急于查看某个可执行文件是否已相对于我们的可执行文件基准进行了篡改,而上述方法可以帮助我们将受影响的块和/或功能归零,尤其是在可执行文件保持相同的可执行标头校验和值。

–约翰·格林
5月11日18:20