在 32 位模式下,每条指令最多 3 个字节长是否可以调用相对地址?

Is it possible to call a relative address with each instruction at most 3 bytes long, in 32-bit mode?

我正在做 x86 汇编(使用 NASM)中的一个练习,它具有 限制每条指令最大数量的利基要求3 个字节.

我想调用一个标签,但执行此操作的正常方法(如代码示例所示)总是导致指令大小为 5 个字节。我正在尝试找出是否有一系列指令(每个指令不超过 3 个字节)可以完成此操作。

我试图将标签地址加载到寄存器中,然后调用该寄存器,但似乎地址随后被解释为绝对地址,而不是相对地址。

我环顾四周,看看是否有办法强制调用将寄存器中的地址解释为相对地址,但找不到任何东西。我考虑过通过将 return 地址推入堆栈并使用 jmp rel8 来模拟调用,但我不确定如何获取我想要 return 到的位置的绝对地址。

这是执行我想要的操作的正常方法:

[BITS 32]

call func     ; this results in a 5-byte call rel32 instruction
; series of instructions here that I would like to return to

func:
  ; some operations here
  ret 

我试过这样的事情:

[BITS 32]

mov eax, func          ; 5-byte  mov r32, imm32
call eax               ; 2-byte  call r32
          ; this fails, seems to interpret func's relative address as an absolute
 ...   ; series of instructions here that I would like to return to

func:
  ; some operations here
  ret 

我觉得可能有一种方法可以使用某种 LEA 魔法来做到这一点,但我对汇编还比较陌生,所以我想不出来。

如有任何提示,我们将不胜感激!

在 32 位 x86 中,读取当前指令指针的唯一方法是执行 call 指令并读取堆栈。除非你在寄存器中已经有了合适的小工具的地址,否则你将不得不使用一个直接的相对偏移量,这是一个 5 字节的指令。

(在64位x86中,你也可以使用lea rax, [rip],但那是一个7字节的指令。)

不过,在这里作弊还是有可能的。如果调用 NASM 二进制文件的代码总是用 call edi 之类的东西调用您的代码,那么您可以从该寄存器计算。这是一个 hack,但将自己限制在 3 字节指令中也是如此。

顺便提一下,这是你如何在 3 字节(或 2 字节)指令中加载 32 位常量的方法(以加载 0xDEADBEEF 为例):

mov al, 0xDE
mov ah, 0xAD
bswap eax
mov ah, 0xBE
mov al, 0xEF

CALL 附近没有相对间接这样的东西。您将不得不找到一些其他机制来调用标签 func。我能想到的一种方法是在寄存器中构建绝对地址并通过寄存器进行绝对间接调用:

不清楚您的代码目标是什么。这假定您正在生成 32 位 Linux 程序。我使用 linker 脚本来计算目标标签的各个字节。程序将使用这些字节在 EAX 中构建一个 return 地址,然后通过 EAX 执行间接近调用.介绍了几种构建地址的方法。

一个 linker 脚本 link.ld 将标签的地址分成单独的字节:

SECTIONS
{
  . = 0x8048000;
  func_b0 =  func & 0x000000ff;
  func_b1 = (func & 0x0000ff00) >> 8;
  func_b2 = (func & 0x00ff0000) >> 16;
  func_b3 = (func & 0xff000000) >> 24;
}

汇编代码文件myprog.asm:

[BITS 32]
global func
extern func_b0, func_b1, func_b2, func_b3

_start:
    ; Method 1
    mov al, func_b3            ; EAX = ######b3
    mov ah, func_b2            ; EAX = ####b2b3
    bswap eax                  ; EAX = b3b2####
    mov ah, func_b1            ; EAX = b3b2b1##
    mov al, func_b0            ; EAX = b3b2b1b0
    call eax

    ; Method 2
    mov ah, func_b3            ; EAX = ####b3##
    mov al, func_b2            ; EAX = ####b3b2
    shl eax, 16                ; EAX = b3b20000
    mov ah, func_b1            ; EAX = b3b2b100
    mov al, func_b0            ; EAX = b3b2b1b0
    call eax

    ; series of instructions here that I would like to return to
    xor eax, eax
    mov ebx, eax               ; EBX = 0 return value
    inc eax                    ; EAX = 1 exit system call
    int 0x80                   ; Do exit system call

func:
    ; some operations here
    ret

Assemble 和 link 以及:

nasm -f elf32 -F dwarf myprog.asm -o myprog.o
gcc -m32 -nostartfiles -g -Tlink.ld myprog.o -o myprog

如果您 运行 objdump -Mintel -Dx 感兴趣的信息看起来类似于:

00000020 g       *ABS*  00000000 func_b0
00000004 g       *ABS*  00000000 func_b2
08048020 g       .text  00000000 func
00000080 g       *ABS*  00000000 func_b1
00000008 g       *ABS*  00000000 func_b3

...

08048000 <_start>:
 8048000:       b0 08                   mov    al,0x8
 8048002:       b4 04                   mov    ah,0x4
 8048004:       0f c8                   bswap  eax
 8048006:       b4 80                   mov    ah,0x80
 8048008:       b0 20                   mov    al,0x20
 804800a:       ff d0                   call   eax
 804800c:       b4 08                   mov    ah,0x8
 804800e:       b0 04                   mov    al,0x4
 8048010:       c1 e0 10                shl    eax,0x10
 8048013:       b4 80                   mov    ah,0x80
 8048015:       b0 20                   mov    al,0x20
 8048017:       ff d0                   call   eax
 8048019:       31 c0                   xor    eax,eax
 804801b:       89 c3                   mov    ebx,eax
 804801d:       40                      inc    eax
 804801e:       cd 80                   int    0x80

08048020 <func>:
 8048020:       c3                      ret

64位代码,2-byte syscall will set RCX = RIP (which the kernel usually uses for sysret), so under most OSes you can make an invalid system call to get RCX=RIP. (e.g. by setting EAX or RAX to -1 with 3-byte or eax,-1, so under Linux syscall will return with RAX = -ENOSYS.)

这取决于 OS 这个方法是否有效:一个 OS 总是可以 return 和 iret 在对寄存器做任何它想做的事情之后,所以它会可以设计一个不起作用的内核 ABI。但 AFAIK 它应该在任何主流 OSes 下工作。但同样,仅在长模式下。 AMD CPU 在 32 位模式下支持 syscall,但工作方式不同。


在 32 位代码中,读取 EIP 的唯一 normal/sane 方法是使用 call 指令。 所以通常不可能创建位置-独立代码,无需使用 5 字节 call rel32 来获取您自己的地址。

(即使是自修改代码最终也会执行一个call rel32)。

其他答案展示了仅使用小指令跳转到给定 绝对 地址的方法。但是目标地址与机器代码的地址无关,除非机器代码的绝对地址已知,因此您可以在构建时计算跳转距离。

如果加载到其他地方,相同的机器代码将跳转到 相同的 地址,而不是相同的偏移 relative 到它自己的地址.

也许这就是您的练习所要求的。


如果不是,由于我们已经排除了编写完全 PIC 代码的合理方式,我们需要考虑疯狂的方式。

中断还将 EIP 推入(内核)堆栈,中断处理程序可以在其中访问它。

如果您正在编写一个可以包含中断处理程序的内核,您可以包含一个将当前地址放入寄存器(例如 EAX)的内核,方法是使用短指令(如 3 字节 mov eax, [ebp+4] 或设置堆栈框架后的任何内容)。

然后您的普通代码可以使用 int 0x81 或其他(3 字节指令)调用该中断处理程序。

如有必要,应该可以设置中断描述符 table:我们可以使用 mov r8,imm8 和移位在寄存器中构造任何值,如其他答案所示。使用这个 + 2 或 3 字节 mov r/m32, r32 或 3 字节 mov r/m8, imm8 我们可以将任何内容存储到我们通过在寄存器中构造地址(和可选值)选择的任何绝对地址。这是为了方便 运行 代码使用紧凑的 "system call" 而不是 call rel32.

来查询自己的地址

实际上可以使用 3 字节 lidt 安装 IDT(0F 01 /3 使用 ModRM + 无额外字节的简单寻址模式)。或者用sidt(等长编码)查询当前位置。

iret 只是 1 个字节 0xCF。我不认为任何必要的系统设置指令的最小长度超过 3 个字节。