如果 x86 jmp 跳转到其他两个连续有效地址之间的地址会发生什么?

What happens if x86 jmp jumps to an address between two other consecutive valid ones?

在 x86 程序集的例程中,如果代码包含指向两个有效地址之间的有效地址的跳转,会发生什么情况?这是一个人为的例子:

0x0001: mov ...
0x0005: add ... 
0x0009: jmp 0x0003

此外,我怎样才能在本地机器或在线上试验这样的东西?我检查了像https://defuse.ca/online-x86-assembler.htm#disassembly这样的在线x86编辑器,但它不允许我输入像“0x0001”这样的指令地址。

没有所谓的“有效”或“无效”地址。每一个地址都可以跳转到,如果对应的页被映射,则执行。

那么当你在指令“之间”跳转时会发生什么?好吧,处理器不知道你打算从哪里开始和结束指令。它只是执行它看到的字节。此代码将与您预期的不同,因为 CPU 试图将其他一些指令的中间部分解析为操作码。

您的具体示例不够具体,我无法说明指令的结果。也许你可以提供一个完整的示例(包括机器代码),以便我给出更好的解释。

CPU会在目标地址开始解码指令。

您在反汇编中看到的指令流(当使用像 objdump 这样的工具时)只是对程序可执行字节的一种解释,假定给定的起点。

碰巧“跳入指令的中间”是一种混淆技术,有时恶意软件会使用它来对线性扫描反汇编程序(如 objdump)隐藏程序语义。更复杂的反汇编程序将尝试遵循这些“未对齐”的跳转,但这可能是不可能的,具体取决于 can/can未确定的内容 statically/dynamically。

论文 "Obfuscation of executable code to improve resistance to static disassembly" by Linn and Debray 对此进行了更详细的讨论。

参见第 3.2 节“垃圾插入”。您描述的场景是他们所说的“部分或完全重叠指令”,即字节流的不同解释可以为重叠地址范围提供不同的汇编指令。

我最近在 codegolf 的“使用 x86/x64 机器代码打高尔夫球的技巧”中添加了一个关于 skipping instructions 的技巧。你会发现那些是跳入先前指令的一部分的有意应用。而且不仅仅是为了混淆。这是该答案的完整文本:

跳过指令

跳过指令是与一个或多个后续操作码组合的操作码片段。后续操作码可以与不同的入口点一起使用,而不是预先设置的跳过指令。使用跳过指令代替无条件短跳转可以节省代码space,速度更快,并设置附带状态如NC(No Carry)。

我的示例都是针对 16 位 Real/Virtual 86 模式的,但是很多这些技术可以类似地用于 16 位保护模式,或者 32 位或 64 位模式。

引用 from my ACEGALS guide:

11: Skipping instructions

The constants __TEST_IMM8, __TEST_IMM16, and __TEST_OFS16_IMM8 are defined to the respective byte strings for these instructions. They can be used to skip subsequent instructions that fit into the following 1, 2, or 3 bytes. However, note that they modify the flags register, including always setting NC. The 16-bit offset plus 16-bit immediate test instruction is not included for these purposes because it might access a word at offset 0FFFFh in a segment. Also, the __TEST_OFS16_IMM8 as provided should only be used in 86M, to avoid accessing data beyond a segment limit. After the db instruction using one of these constants, a parenthetical remark should list which instructions are skipped.

86 模式定义 in lmacros1.mac 323cc150061e (2021-08-29 21:45:54 +0200):

%define __TEST_IMM8 0A8h                        ; changes flags, NC
%define __TEST_IMM16 0A9h                       ; changes flags, NC
                                        ; Longer NOPs require two bytes, like a short jump does.
                                        ; However they execute faster than unconditional jumps.
                                        ; This one reads random data in the stack segment.
                                        ;  (Search for better ones.)
%define __TEST_OFS16_IMM8 0F6h,86h              ; changes flags, NC

16 位模式下的 0F6h,86h 操作码是 test byte [bp + disp16], imm8 指令。我相信我实际上并没有在任何地方使用这个。 (事实上​​,堆栈内存访问实际上可能比无条件短跳转慢。)

0A8h 是任何模式下 test al, imm8 的操作码。 0A9h 操作码在 32 位和 64 位模式下变为 test eax, imm32 形式的指令。

两个用例in ldosboot boot32.asm 07f4ba0ef8cd (2021-09-10 22:45:32 +0200):

首先,为一个公共函数链接两个不同的入口点,这两个入口点都需要初始化一个字节大小的寄存器。 mov al, X 指令每条占用 2 个字节,因此 __TEST_IMM16 可用于跳过一条这样的指令。 (如果有两个以上的入口点,可以重复此模式。)

error_fsiboot:
        mov al,'I'

        db __TEST_IMM16 ; (skip mov)
read_sector.err:
        mov al, 'R'     ; Disk 'R'ead error

error:

其次,某个入口点需要两个字节的额外拆解,但可以与后面代码部分的 fallthrough case 共享。

                mov bx, [VAR(para_per_sector)]
                sub word [VAR(paras_left)], bx
                jbe @F          ; read enough -->

                loop @BB
                pop bx
                pop cx

                call clust_next
                jnc next_load_cluster
                inc ax
                inc ax
                test al, 8      ; set in 0FFF_FFF8h--0FFF_FFFFh,
                                ;  clear in 0, 1, and 0FFF_FFF7h
                jz fsiboot_error_badchain
                db __TEST_IMM16
@@:
                pop bx
                pop cx
                call check_enough
                jmp near word [VAR(fsiboot_table.success)]

这是一个用例 in inicomp lz4.asm 4d568330924c (2021-09-03 16:59:42 +0200),我们依赖 test al, X 指令清除进位标志:

.success:
        db __TEST_IMM8                  ; (NC)
.error:
        stc
        retn

最后,这里是 DOSLFN Version 0.41c (11/2012) 中跳转指令的非常相似的用法。他们使用 mov cx, imm16 而不是 test ax, imm16,它对状态标志没有影响,但会破坏 cx 寄存器。 (操作码 0B9h 在非 16 位模式下为 mov ecx, imm32,并写入完整的 ecxrcx 寄存器。)

;THROW-Geschichten... [english: THROW stories...]
SetErr18:
        mov     al,18
        db      0B9h            ;mov cx,nnnn
SetErr5:
        mov     al,5
        db      0B9h            ;mov cx,nnnn
SetErr3:
        mov     al,3
        db      0B9h            ;mov cx,nnnn
SetErr2:
        mov     al,2
SetError: