英特尔 X86 汇编:如何分辨许多位宽是一个争论



在以下程序集中:

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

将其视为组装的 C 函数,arg_0有多少位宽(C 函数的参数)?var_8多少位宽(局部 C 变量)?也就是说,它是短的,整数的,等等。

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

如果程序集还包含以下行:

ecx, [ebp+arg_0]

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

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

  1. 汇编程序必须能够推断出正确的长度。
    尽管英特尔的语法没有像AT&T语法那样使用大小后缀,但汇编程序仍然需要一种方法来查找操作数的大小。
     
    如果存储的大小为 32 位(请注意后缀l),则不明确的指令mov [var], 1在 AT&T 语法中编写为movl $1, var,因此很容易分辨出直接操作数的大小。
    接受英特尔语法的汇编程序需要一种方法来推断此大小,有四种广泛使用的选项:

    • 它是从另一个操作数推断出来的。
      例如,当涉及寄存器时就是这种情况。
      例如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位,存储也是如此(默认情况下)。
      这是我不喜欢的形式,因为它使代码更难阅读(汇编的一个好处是指令语义的"局部性"),并将高级概念(如类型)与低级概念(如商店规模)混合在一起。 这就是为什么 NASM 的语法不支持魔法/非本地大小关联的原因。

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

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

    如果您正在查看反汇编的代码,反汇编程序通常采取安全方面,并始终明确说明大小。
    如果没有,您必须求助于手动检查操作码,如果反汇编器不显示操作码,是时候更改它了。
    反汇编程序可以轻松找出操作数的大小,因为它正在反汇编的二进制代码与 CPU 执行的二进制代码相同,并且指令操作码对操作数大小进行编码。
     

  2. C
  3. 语言故意松散于 C 类型如何映射到位数
     
    试图从反汇编中推断变量的类型并不是徒劳的,但也必须考虑平台,而不仅仅是架构。
    这里讨论使用的主要模型:

    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 on x86_64 使用 LLP64。 x86-64 上的其他操作系统通常使用 x86-64 System V ABI,一种 LP64 型号。

  4. 汇编没有类型,程序员可以利用这一点
     
    ,即使是编译器也可以利用它。
     
    在链接的情况下,类型为long long(64 位)的bar变量使用 1 进行 OR 运算,clang仅对低字节进行 OR 运算,从而保留 REX 前缀。 如果立即使用两个 dword 加载或一个 qword 重新加载变量,这会导致存储转发停止,因此它可能不是一个好的选择,尤其是在 32 位模式下,or dword [bar], 1大小相同,并且很可能被重新加载为两个 32 位半。
    如果不小心看拆解的代码,他们可以推断bar是8位的。
    这种部分访问变量或对象的技巧很常见。
     
    为了正确猜测变量的大小,需要一些专业知识。
    例如,结构成员通常是填充的,因此它们之间有未使用的空间,这可能会欺骗没有经验的用户认为每个成员都比实际大。
    堆栈具有精确的对齐要求,这也可能使参数尺寸变宽。
     
    经验法则是,编译器通常更喜欢保持堆栈 16 字节对齐,并自然对齐所有变量。 多个窄变量被打包到一个dword中。 当通过堆栈传递函数参数时,每个参数都填充到 32 位或 64 位,但这不适用于堆栈上局部变量的布局。

最终回答您的问题

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

如果一个mov ecx, [ebp+arg_0]出现在代码的后面,你将不得不重新审视你对arg_0值大小的猜测,它肯定至少是32位的。
它不太可能是 64 位(64 位类型在 32 位代码中很少见,我们可以打赌),因此我们可以得出结论它是 32 位。
显然,第一个代码段是仅使用变量的一部分的技巧之一。

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

最新更新