汇编溢出

本文详细解析了在x86/x64架构中,溢出标志OF、符号标志SF及进位标志CF的工作原理,并介绍了如何利用这些标志进行条件判断。
  二个异符号数相加,OF=0  
  二个同符号数相加,结果与加数符号不同,OF=1  
  二个同符号数相减,OF=0  

  二个异符号数相减,结果与减数符号相同,OF=1

==============================================

在 rflags 寄存器里的下面三个标志位记录溢出, 符号和进位状态:

  • OF(Overflow Flag)
  • SF(Sign Flag)
  • CF(Carry Flag)

1. 溢出产生的条件

我们看看下面这个式子,为了简单,作为例子我用 4 位数计算。

1.1 产生溢出

式子1:                                               式子2:
     0 1 1 1             ( 7 )                              0 0 1 1         ( 3 )
+    0 1 1 1             ( 7 )                        +     0 0 1 1         ( 3 )
-------------------------------                       ---------------------------
     1 1 1 0             ( -2 ) 溢出                         0 1 1 0         ( 6 ) 正确

上面的式子中,式子1 在 unsigned 数前提下 7 加 7 结果等于 14,这个结果看似正确。实际上它产生了 Overflow(溢出),结果超出了 4 位数能表达的范围,这是因为:当用来表达 signed(符号数)时,结果值 1110 却是 -2,那么 7 + 7 = -2 这显示是错误的,因此这里产生了溢出(超过了表达范围)。而式子2 是正确的。

在上面两个式子中,我们可以得到规律:

正数 + 正数 = 负数,就产生了溢出

接着再看下面的式子:

式子1:                                               式子2:
     1 1 0 0             ( -4 )                             1 1 0 0        ( -4 )
+    1 0 0 0             ( -8 )                      +      1 1 1 1        ( -1 )
-------------------------------                      ----------------------------
   1 0 1 0 0             (  4 ) 溢出                       1 1 0 1 1        ( -5 ) 正确
  --                                                      -- 
  进位                                                    进位

再来看看上面两个式子,式子1 在 singed 数的前提下 (-4) + (-8) = 4,结果很显然是错误的,因此这个式子也产生了溢出。而式子2 中, (-4) + (-1) = (-5) 这是正确的。

同样我们得到规律是:

负数 + 负数 = 正数,就产生了溢出

1.2 不产生溢出

       1 1 1 1                 ( -1 )
+      0 1 1 1                 ( 7 )
--------------------------------------
    1  0 1 1 0                 ( 6 ) 正确
   ---
   进位

最后再看看这个式子,在 signed 数的情况下:(-1) + 7 = 6 这个结果是正确的,因此这个式子不会产生溢出。

1.3 溢出总结

现在我们可以总结产生溢出的条件:

  1. 两个正数相加,如果结果为负数,就产生了溢出。
  2. 两个负数相加,如果结果为正数,就产生了溢出。

换个角度来说:

  1. 正数 - 负数 = 负数,就产生了溢出。
  2. 负数 - 正数 = 正数,就产生了溢出。

那么是什么情况不会产生溢出的?

  • 不同符号的两个数相加,不会产生溢出。

那么我们也可以得出同符号数相减,也不会产生溢出,例如:(-1) - (-1) = -1 + 1 = 0 这个式子是两个负数相减,等同于负数加正数,这不会产生溢出。

2. 产生进位与借位

上面 5 个式子中,有 3 个产生了进位情况,以 4 位数为例,当计算的结果高最位向上进一位时,就会产生进位情况,看看其中一个式子:

        1 1 0 0        ( -4 )
 +      1 1 1 1        ( -1 )
 ----------------------------
      1 1 0 1 1        ( -5 ) 正确
     -- 
    进位

计算结果向上进了 1 位,结果依然是正确的,rflags.CF 被置为 1,rflags.OF 被清 0,如果被作为 unsigned 数进行计算时:12 + 15 = 27,4 位数表达的结果为 11,这时需要得到正确结果,需借助 CF 标志。在这里 CF 被置位。

当然这里还会产生一个借位的情况,下面这个式子:

       0 1 0 1         ( 5 )
-      1 0 0 0         ( 8 )
----------------------------
     1 1 1 0 1         ( -3 )
    --
   借位

两个数相减,最高位不足减,向前借位,这种情况下也会产生 rflags.CF = 1

3. 符号位

数的最高位描述符号位,以 4 位数为例,最高位 Bit 3 是符号位,rflags.SF 会根据计算结果置相应的位,rflags.SF = 1 时表示为负数,而 rflags.SF = 0 则为正数。

4. 溢出,符号与进位的实际应用

它们较多应用在一些条件判断的场合,典型地根据两个数比较结果,做相应的处理。由数存在 unsigned 和 singed 因此在计算机中需要提供 unisgned 数和 singed 数的计算的解决方案。下面我们看看 OF, SFCF 标志位在 x86/x64 平台中条件判断中的应用。

4.1 signed 符号数的比较

首先我们来了解 signed 的条件判断,下面的代码:

signed int a;
signed int b;


if (a > b) {
    ... ...

} else if (a < b) {
    ... ...

} else if (a == b) {
   ... ...
}

那么我可以使用下面的指令来描述上面的 C 代码:

    mov eax, a
    cmp eax, b

    je equal                ; a == b ?
    jg great                ; a > b ?

    ... ...                 ; No! a < b

    jmp next

great:              
    ... ...                 ; Yes! a > b

    jmp next

equal:
    ... ...                 ; Yes! a == b

next:
    ... ...

在执行 cmp 指令后,processor 会设置 rflags 的标志位,cmp 指令实际上做减法处理,根据计算结果来更新 rflags 标志位。

4.1.1 等于

等于情况的判断比较容易理解,通过判断 rflags.ZF 标志,当 rflags.ZF = 1 时,表示 cmp 结果相等。使用指令 jzje 来进行判断,它们是同一个指令 opcode

4.1.2 大于

我们看看 processor 是如何判断符号数的大于以及小于的情况,假如有下面两个比较式子:

  • -1 > -2
  • 4 > -6

前面说过 cmp 指令做减法计算,我们来看看这个过程,同样以 4 位数作为例子

式子1:                                              式子2:

-1 - (-2):                                         4 - (-6):

     1 1 1 1    ( -1 )                                    0 1 0 0    ( 4 )
-    1 1 1 0    ( -2 )                               -    1 0 1 0    ( -6 )
-----------------------                             -----------------------
     0 0 0 1    ( 1 )                                     1 0 1 0    ( -6 ) 溢出
                                           

SF = 0                                                SF = 1
OF = 0                                                OF = 1
CF = 0                                                CF = 1
ZF = 0                                                ZF = 0

我们看到对于 -1 > -2 这个比较结果是:

  • SF = 0
  • OF = 0
  • CF = 0
  • ZF = 0

对于 4 > -6 这个比较结果是:

  • SF = 1
  • OF = 1
  • CF = 1
  • ZF = 0

显然这两个比较的式子都是 true 的,这个式子中也产生了 carry(也即是借进),它们相同的地方就是 SF = OF,因此我们可以判断:

SF = OF 时,比较结果是大于。

4.1.3 小于

现在我们来看看小于的情况是如何的?同样有下面的两个比较式子:

  • -1 > 2
  • -3 > 6

这两个式子我们知道,它们都是 false 的,同样我们来看看如何比较:

式子1:                                              式子2:

-1 - 2:                                             -3 - 6:

     1 1 1 1    ( -1 )                                    1 1 0 1    ( -3 )
-    0 0 1 0    ( 2 )                                -    0 1 1 0    ( 6 )
-----------------------                             -----------------------
    1 1 0 1    ( -3 )                                    0 1 1 1    ( 7 ) 溢出
                                                   
                                                  
SF = 1                                                SF = 0
OF = 0                                                OF = 1
CF = 0                                                CF = 0
ZF = 0                                                ZF = 0

式子 -1 > 2 的比较结果是:

  • SF = 1
  • OF = 0
  • CF = 0
  • ZF = 0

式子 -3 > 6 的比较结果是:

  • SF = 0
  • OF = 1
  • CF = 0
  • ZF = 0

这两个式子都是 false 的,它们相同之处就是 SF <> OF 我们这里得出来的结果是:

SF <> OF 时,比较结果是小于

4.1.4 signed 数的条件跳转指令

x86 提供了一系列的条件跳转指令来处理条件判断,下面是基本 signed 符号数的条件判断指令:

  • jg 大于
  • jge 大于或等于
  • jl 小于
  • jle 小于或等于

上面这 4 条是基本 singed 数判断指令,然而根据不同的表达词,又可以变化出多种助记符,例如:

  • jg 可以写成 jnle(不小于等于)
  • jge 可以写成 jnl(不小于)
  • jl 可以写成 jnge(不大于等于)
  • jle 可以写成 jng(不大于)

加上了一些否定修饰产生同样效果的指令,实际上这些对应的否定词指令是同一个 opcode 的不同表现形式,使用哪个,依据个人喜好。

这些指令正是使用了 rflags 寄存器的 SF, OF 以及 ZF 进行条件判断,例如:

  • jg 指令:当 ZF = 0 并且 SF = OF 时就跳转
  • jl 指令:当 SF <> OF 时就跳转,这里不需要判断(ZF = 0),仅需判断 SF <> OF 条件就足够了,因为:当 ZF = 1 时,必然 SF = OF
  • jge 指令:当 SF = OF 时就跳转,这里不需要判断(ZF = 1),仅需判断 SF = OF 条件就足够了(因为:无论 ZF 是否为 1 都满足大于或等于的条件
  • jle 指令:当 ZF = 1 或者 SF <> OF 时就跳转转,结果为 0 也满足条件(也就是当 ZF = 1(此时 SF = OF) 或者 SF <> OF 的情况下,都满足小于或等于的条件)

4.2 unsigned 无符号数的比较

对于无符号数来说,判断大小则简单多了,我们将上面的 C 代码改一改,a 和 b 定义为 unsigned 无符号:

unsigned int a;
unsigned int b;

if (a > b) {
    ... ...

} else if (a < b) {
   ... ...  
 
} else if (a == b) {
   ... ...
}

同样我可以用以下汇编指令描述为:

    mov eax, a
    cmp eax, b

    je equal              ; a == b ?
    ja above              ; a > b ?

below:
    ... ...               ; No! a < b
  
    jmp next

above:
    ... ...               ; Yes! a > b

    jmp next

equal:
    ... ...               ; Yes! a == b

next :
    ... ...

结构上是和 singed 数是一致的,只是使用了不同的指令。

4.2.1 大于

我们来看看 unsigned 数的大于情况,下面两个比较式子:

  • 12 > 3
  • 12 > 6

式子1:                                     式子2:

12 - 3:                                     12 - 6:

       1 1 0 0        ( 12 )                         1 1 0 0      ( 12 )
-      0 0 1 1        ( 3 )                 -        0 1 1 0      ( 6 )
----------------------------                ----------------------------
       1 0 0 1        ( 9 )                          0 1 1 0      ( 6 )

SF = 1                                      SF = 0
OF = 0                                      OF = 1
CF = 0                                      CF = 0
ZF = 0                                      ZF = 0

显然我们知道这两个式子都是 true 的,从上面的计算式子我们可以得到,对于 unsigned 数的比较:

当 CF = 0 时,比较结果大于

当 CF = 0 时表明计算结果没有产生借位,因此比较结果是大于。另外我们可以看出,当这个式子是 signe 符号数时,结果是小于。

4.2.2 小于

下面两个条件判断式子:

  • 7 > 8
  • 5 > 7

式子1:                                     式子2:

7 - 8 :                                     5 - 7:

       0 1 1 1        ( 7 )                         0 1 0 1      ( 5 )
-      1 0 0 0        ( 8 )                 -       0 1 1 1      ( 7 )
----------------------------                ----------------------------
       1 1 1 1        ( 15 )                        1 1 1 0      ( 14 )

SF = 1                                      SF = 1
OF = 1                                      OF = 0
CF = 1                                      CF = 1
ZF = 0                                      ZF = 0

由上面的计算式子可以看到:

当 CF = 1 时,比较结果是小于

同样当 CF = 1 时,表示被减数不足,需要借进,因此它是小于的。

4.2.3 unsigned 数的条件跳转指令

x86 提供了 4 个基本的 unsigned 数条件跳转指令:

  • ja 高于
  • jae 高于等于
  • jb 低于
  • jbe 低于等于

同样也可能加上否定修饰,变成以下几种形式:

  • ja 也可以为 jnbe(不低于等于)
  • jae 也可以为 jnb(不低于)
  • jb 也可以为 jnae (不高于等于)
  • jbe 也可以为 jna(不高于)

它们是依据 CFZF 进行判断:

  • ja 指令:当 CF = 0 且 ZF = 0 时跳转
  • jb 指令:当 CF = 1 且 ZF = 0 时跳转
  • jae 指令:当 ZF = 1 或者 CF = 0 时跳转
  • jbe 指令:当 ZF = 1 或者 CF = 1 时跳转

jb 指令的另一个形式可以为 jc(当 CF = 1 时跳转)。

5. eflags 标志位的设置

x86/x64 平台里的加减类运算中,指令会根据 unsigned 和 singed 结果设置 eflags 标志位,下面是 Intel 手册的一段话:

The SUB instruction performs integer subtraction. It evaluates the result for bothsigned and unsigned integer operands and sets the OF and CF flags to indicate anoverflow in the signed or unsigned result, respectively. The SF flag indicates the signof the signed result.

指令会对结果进行一个评估,评估根据操作数的 unsigned 和 signed 两方面。

那么,我们可以做出下面的推断,还是以 4 位数减法做为一个例子:

        0 1 1 1      ( 7 )
-       1 0 0 0      ( 8 )
--------------------------
        1 1 1 1      ( 15 )

这个二进制减法的结果的 1111B,sub 指令对这个结果进行评估:

  • 对于 unsigned operand 来说:eflags.CF = 1
  • 对于 singed operand 来说:正数减负数,结果为负数,结果溢出了,eflags.OF = 1efalgs.SF = 1

条件跳转指令可以对计算指令区分 unsigned 和 singed 两种情况:

mov eax, 0x70000000
mov ebx, 0x80000000

sub eax, ebx                                 ; 0x70000000 - 0x80000000

在例子中的 0x70000000 - 0x80000000 的减法计算中:

  • 对于 singed 计算结果,设置 eflags.OF = 1 和 eflags.SF = 1
  • 对于 unsinged 计算结果,设置 eflags.CF = 1

那么,我们可以使用两种条件跳转指令:

sub eax, ebx

jg .L1                                       ; signed 跳转指令

ja .L2                                       ; unsigned 跳转指令


<think>首先,用户的问题是:“我想查找汇编语言中栈溢出的示例 请问汇编语言栈溢出示例”。这直接要求我提供一个栈溢出的示例。 回顾系统级指令: - 所有行内数学表达式必须用 $...$,但我这里不太涉及数学表达式,主要是汇编代码。 - 独立公式或代码块应该用 $$...$$ 或单独的代码块。 - 使用中文回答。 - 生成相关问题。 - 引用末尾添加引用标识。 - 回答结构清晰,逐步解决问题。 我的回答应该基于提供的引用。用户给出了多个引用: - [^1]: 涉及栈溢出漏洞,strcpy导致溢出。 - [^2]: 解释栈操作(push, pop),栈溢出原理。 - [^3]: 展示函数参数压栈。 - [^4]: 调试示例,read函数溢出漏洞。 - [^5]: 64位汇编代码,涉及栈操作。 我需要从这些引用中提取栈溢出的示例。引用[^1]、[^4]、[^5]似乎提供了具体例子。 结构化回答: 1. **介绍栈溢出概念**:简要解释什么是栈溢出,参考引用。 2. **提供示例**:展示一个汇编语言栈溢出示例的代码和解释。 3. **解释原因**:说明为什么会导致溢出。 4. **生成相关问题**:在末尾添加相关问题。 关键示例: - 从引用[^4]:它提到编译代码时关闭栈保护,然后反汇编,展示read函数输入超过缓冲区大小导致溢出。 - 引用[^1]:strcpy函数导致溢出。 - 引用[^5]:有64位汇编代码,但可能需要简化。 我应该选择一个清晰的示例。最佳选择是引用[^4],因为它直接描述了栈溢出漏洞。 构建一个简单的示例: - 假设一个函数有局部变量缓冲区(如char buf[20]),但使用像read这样的函数读取超过20字节的数据。 - 在汇编中展示栈帧。 例如,参考引用[^4],它说: - 编译test.c with -fno-stack-protector - 反汇编主函数和add函数 - read函数可以输入100字节,但buf只有20字节空间。 我可以在伪汇编中展示这个。 确保回答真实可靠:基于提供的引用,不要添加虚构内容。 回答结构: 1. **引言**:定义栈溢出。 2. **示例描述**:描述一个栈溢出场景。 3. **汇编代码示例**:提供代码块展示相关部分。 4. **解释**:解释溢出如何发生。 5. **结论**:简要总结。 6. **相关问题**:生成2-3个相关问题。 使用中文。 最终输出: - 文本解释。 - 代码块用反引号包裹。 - 相关问题部分。 </think>### 汇编语言栈溢出示例 在汇编语言中,栈溢出(Stack Overflow)是一种常见的安全漏洞,它发生在程序向栈段(Stack Segment)写入数据时超过了预留的缓冲区空间,覆盖了返回地址或其他关键数据,导致程序崩溃或被恶意利用。栈是先进后出(LIFO)的内存区域,由SS:SP寄存器指向栈顶;当函数调用时(如通过call指令),参数和局部变量会被压栈,如果写入数据过多就可能覆盖相邻内存[^2][^4]。下面我将基于提供的引用内容,逐步展示一个简洁的栈溢出汇编示例,并解释其机制。 #### 栈溢出示例描述 考虑一个简单的C函数,其中定义了缓冲区 `buf[20]`(预留20字节空间),但使用 `read` 函数读取用户输入时允许输入100字节。在汇编层面,这会导致写入超出缓冲区边界,覆盖栈帧中的返回地址或其他变量。引用[^4]中的实验展示了这种漏洞:当输入超过缓冲区大小时,数据会“溢出”到高地址区域,破坏栈的正常结构。关键点包括: - 函数调用时,栈帧包含局部变量、返回地址等。 - 如果写入数据量大于缓冲区大小,就会覆盖返回地址(例如,导致程序跳转到错误地址)。 - 在实际汇编中,这通常通过系统调用(如read)实现。 #### 汇编代码示例 以下是一个简化的32位x86汇编示例(基于引用[^4]的调试场景)。假设有一个函数 `vulnerable_function`,它在栈上分配了20字节缓冲区,但调用read函数时可读取100字节。关闭栈保护编译后(如gcc -fno-stack-protector),反汇编代码会显示漏洞。汇编片段如下: ```assembly section .data buf_size equ 20 ; 预设缓冲区大小 input_size equ 100 ; 允许输入的大小,超过了缓冲区 section .text global _start _start: call vulnerable_function ; ... 其他代码 vulnerable_function: push ebp ; 保存基址指针 mov ebp, esp ; 设置新栈帧 sub esp, buf_size ; 为buf分配20字节栈空间 (地址: ebp-20到ebp-1) ; 调用read函数(模拟系统调用) mov eax, 3 ; sys_read 系统调用号 mov ebx, 0 ; 文件描述符0(标准输入) lea ecx, [ebp-20] ; buf的地址(栈缓冲区起始地址) mov edx, input_size ; 输入大小100字节(超过buf_size) int 0x80 ; 触发系统调用 add esp, buf_size ; 理论上释放栈空间(但溢出已发生) pop ebp ; 恢复基址指针 ret ; 返回,此时返回地址可能已被覆盖 ``` #### 解释溢出机制 1. **栈帧结构**:当 `vulnerable_function` 被调用时,`call` 指令先将返回地址压栈,然后 `push ebp` 保存原基址。`sub esp, buf_size` 在栈顶分配20字节给 `buf`(地址范围 ebp-20 到 ebp-1)[^2][^3]。 2. **溢出发生**:`read` 系统调用从标准输入读取最多100字节数据到 `buf`。但由于 `buf` 只有20字节空间: - 写入前20字节正常填充缓冲区。 - 之后80字节会向高地址溢出:覆盖 ebp 保存值(ebp 位置),接着覆盖返回地址(在 ebp+4 位置),甚至更高地址的数据。 - 如引用[^1]所述,类似 `strcpy` 操作,截断字符(如 NULL 字节)也可能被写入未分配区域。 3. **后果**:当函数执行 `ret` 指令时,它会从栈中弹出返回地址。如果该地址被覆盖为无效值(如0x00或其他恶意地址),程序会崩溃或执行任意代码。在引用[^4]的实验中,这导致未授权访问(如打印 "congratulations"),演示了安全风险。 4. **关键点**:栈溢出依赖于缓冲区大小与实际输入的不匹配。在64位汇编(如引用[^5])中机制类似,但寄存器(如 rbp, rsp)和数据尺寸更大。 #### 总结 这个示例展示了汇编语言中栈溢出的基本原理:不当的内存写入超出缓冲区边界,破坏栈帧完整性。在实际应用中,这常用于逆向工程或安全测试(如引用[^1][^4])。为了防止此类漏洞,开发者应使用安全函数(如 strncpy 代替 strcpy)并启用栈保护机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值