英特尔 X86 汇编:如何判断多位宽是一个论点?

Intel X86 Assembly: How to tell many bits wide is an argument?

在以下程序集中:

mov     dx, word ptr [ebp+arg_0]
mov     [ebp+var_8], dx

将其视为一个汇编的 C 函数,(C 函数的参数)有多少位宽arg_0? (局部 C 变量)var_8 是多少位宽?也就是是short,int之类的

由此看来,var_8是16位的,因为dx是一个16位的寄存器。但我不确定 arg_0。

如果程序集也包含这一行:

ecx, [ebp+arg_0]

这是否意味着 arg_0 是一个 32 位值?

要解决这个问题,需要理解三个原则。

  1. 汇编程序必须能够推断出正确的长度。
    虽然英特尔的语法没有使用像 AT&T syntax 这样的大小后缀,但汇编器仍然需要一种方法来找到操作数的大小。

    有歧义的指令mov [var], 1在AT&T语法中写成movl , var,如果store的大小是32位(注意后缀l),那么很容易分辨出大小立即数。
    接受 Intel 语法的汇编程序需要一种方法来推断这个大小,有四个广泛使用的选项:

    • 是从另一个操作数推断出来的
      例如,当涉及寄存器时就是这种情况。
      例如。 mov [var], dx 是 16 位存储。
    • 明确说明。
      mov WORD [var], dx
      MASM 语法汇编程序在大小后需要一个 PTR,因为它们的大小说明符只允许在内存操作数上使用,而不是立即数或其他任何地方。
      这是我更喜欢的形式,因为它清晰、突出并且不太容易出错(mov WORD [var], edx 无效)。
    • 根据上下文推断

       var db 0
      
       mov [var], 1   ; MASM/TASM only.   associate sizes with labels 
      

      MASM 语法汇编程序可以推断,由于 var 是用 db 声明的,因此它的大小是 8 位,存储也是如此(默认情况下)。
      这是我不喜欢的形式,因为它使代码更难阅读(关于汇编的一个好处是指令语义的 "locality")并且将高级概念(如类型)与低级概念混合比如商店的大小。这就是 NASM's syntax doesn't support magical / non-local size association 的原因。

    • 绝大多数时候只有一种尺寸是正确的
      push、分支和所有操作数大小取决于内存模型或代码大小的指令就是这种情况。
      某些指令可以覆盖实际使用的大小,但默认值是一个明智的选择。 (例如 push word 123 vs. push 123


    简而言之,必须是汇编程序告诉大小的一种方式,否则它将拒绝该代码。 (或者一些低质量的汇编器,如 emu8086,对于不明确的情况有默认的操作数大小。)

    如果您正在查看反汇编代码,反汇编程序通常会采取安全措施并始终明确说明大小。
    如果不是,您必须求助于手动检查操作码,如果反汇编器不显示操作码,则该更改它了。
    反汇编器很容易找出操作数的大小,因为它正在反汇编的二进制代码与 CPU 执行的二进制代码相同,指令操作码对操作数大小进行编码。

  2. C 语言在 C 类型如何映射到位数方面有意放宽

    尝试从反汇编中推断变量的类型并非徒劳,但必须同时考虑平台,而不仅仅是架构。
    讨论使用的主要模型 here:

    Datatype    LP64    ILP64   LLP64   ILP32   LP32
    char        8       8       8       8       8
    short       16      16      16      16      16
    _int32      32          
    int         32      64      32      32      16
    long        64      64      32      32      32
    long long                   64      [64]                    
    pointer     64      64      64      32      32
    

    Windows 在 x86_64 上使用 LLP64。 x86-64 上的其他操作系统通常使用 x86-64 System V ABI,一种 LP64 模型。

  3. 程序集没有类型,程序员可以利用它

    Even compilers can exploit that.

    在链接类型 long long(64 位)的 bar 变量与 1 进行“或”运算的情况下,clang 通过仅对低字节进行“或”运算来节省 REX 前缀。如果立即使用两个双字加载或一个 qword 重新加载变量,这会导致存储转发停顿,因此这可能不是一个好的选择,尤其是在 or dword [bar], 1 大小相同的 32 位模式下,它很可能重新加载为两个 32 位的一半。
    如果不小心看一下反汇编代码,他们可以推断出 bar 是 8 位。
    这种部分访问变量或对象的技巧很常见。

    为了正确猜测变量的大小,需要一些专业知识。
    例如,结构成员通常被填充,因此它们之间有未使用的 space,这可能会让没有经验的用户误以为每个成员都比实际大。
    堆栈具有精确的对齐要求,也 may make widen the parameters size.

    经验法则是编译器通常更喜欢保持堆栈 16 字节对齐,并自然对齐所有变量。 Multiple narrow variables are packed into a single dword。通过堆栈传递函数参数时,每个参数都被填充为 32 位或 64 位,但这不适用于堆栈上局部变量的布局。

最后回答你的问题

是的,从第一段代码可以假设 arg_0 的值是 16 位宽。
请注意,由于它是在堆栈上传递的函数 arg,因此它实际上是 32 位的,但未使用高 16 位。

如果 mov ecx, [ebp+arg_0] 在代码中出现的时间晚于您对 arg_0 值大小的猜测,那么它肯定至少是 32 位。
它不太可能是64位的(64位类型在32位代码中很少见,我们可以打赌)所以我们可以断定它是32位的。
显然,第一个片段是那些只使用变量的一部分的技巧之一。

这就是你如何处理一个 var 大小的逆向工程,你猜测,验证它是否与其余代码一致,如果不一致,请重新访问,重复。
随着时间的推移,您将做出基本不需要修改的大部分正确猜测。