Error[8]: Undefined offset: 140, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况?

TL:DR

int 0x80
只要正确使用任何指针,只要任何指针都适合32位( 堆栈指针不适合 ), 它就可以工作 。同样,将其
strace

解码错误
,将寄存器内容当作64位
syscall
ABI进行解码。(到目前为止,还没有一种简单/可靠的方法
strace
可以告诉您。)

int 0x80
将r8-r11归零,并保留其他所有内容。就像在32位代码中使用32位电话号码一样使用它。(或者更好,不要使用它!)

并非所有系统都支持

int 0x80
:Linux的Windows子系统(WSL)严格来说仅是64位:
int0x80
根本不起作用。也可以在没有IA-32仿真的情况下构建Linux内核。(不支持32位可执行文件,不支持32位系统调用)。


详细信息:保存/恢复了什么,内核使用了哪些部分

int 0x80
使用
eax
(不是完整的
rax
)作为系统调用号,调度到32位用户空间
int0x80
使用的同一函数指针表。(这些指针指向
sys_whatever
内核内部本机64位实现的实现或包装。系统调用实际上是跨越用户/内核边界的函数调用。)

仅传递arg寄存器的低32位。 的上半部分

rbx
-
rbp
被保留,但忽略了
int 0x80
系统调用。

请注意,将错误的指针传递给系统调用不会导致SIGSEGV。而是系统调用返回
-EFAULT
。如果您不检查错误返回值(使用调试器或跟踪工具),它将似乎无提示地失败。

除了 r8-r11被清零 之外,所有寄存器(当然是eax除外)都被保存/恢复(包括RFLAGS和整数regs的高32位)。

r12-r15
在x86-64 SysV ABI的函数调用约定中保留了调用,因此
int0x80
64位清零的寄存器是AMD64添加的“新”寄存器的调用集中子集。

通过对内核内部实现寄存器保存的方式进行一些内部更改,保留了此行为,并且内核中的注释提到它可以在64位上使用,因此此ABI可能是稳定的。(即,您可以指望r8-r11被清零,并且所有其他内容都将保留。)

返回值被符号扩展以填充64位

rax

(Linux将32位sys_函数声明为返回有符号的
long
。)这意味着
void*mmap()
在64位寻址模式下使用之前,指针返回值(如from )需要零扩展。

与不同

sysenter
,它保留的原始值
cs
,因此它以与调用时相同的方式返回到用户空间。(使用
sysenter
结果将内核设置
cs
$__USER32_CS
,这将为32位代码段选择一个描述符。)


strace``int 0x80
为64位进程 解码 不正确 。它将好像处理已使用
syscall
而不是进行解码
int0x80
。 这可能非常令人困惑。例如因为
strace
打印的
write(0, NULL, 12 <unfinished ... exitstatus 1>
eax=1
/
int 
_exit(ebx)
x80
,实际上
write(rdi, rsi, rdx)
不是
int 0x80


0x00000000
只要所有参数(包括指针)都适合寄存器的低32位,它就起作用。x86-64 SysV
ABI中默认代码模型(“小”)中的静态代码和数据就是这种情况。(第3.5.1:
所有的符号被称为是位于该范围内的虚拟地址
0x7effffff
mov edi,hello
,所以你可以做一样的东西
mov $hello, %edi
(AT&T 不是),以获得一个指针与5个字节的指令寄存器)。

但是, 这是

gcc
对的情况下与位置无关的可执行文件,其中许多Linux发行版现在配置
hello.c
默认情况下,使
(他们启用ASLR的可执行文件)。例如,我
puts
在Arch
Linux上编译了一个,并在main的开始处设置了一个断点。传递给的字符串常量
0x555555554724
为at
write
,因此32位ABI
rsp
系统调用将不起作用。(GDB默认情况下会禁用ASLR,因此,如果您从GDB内部运行,则每次运行时总是看到相同的地址。)

Linux将堆栈放在规范地址的上限和下限之间的“间隙”附近,即,堆栈的顶部为2 ^
48-1。(或者是随机的,启用了ASLR)。因此,

_start
在进入
0x7fffffffe550
典型的静态链接可执行文件时
esp
,具体取决于env
vars和args的大小。截断此指针
-EFAULT
不会指向任何有效的内存,因此,
rsp
如果您尝试传递截断的堆栈指针,则通常会返回带有指针输入的系统调用。(如果你截断你的程序会崩溃
esp
arch/x86/entry/entry_64_compat.S
,然后做与堆栈,例如,如果你建立的32位汇编源作为64位可执行任何东西。)


它如何在内核中工作:

在Linux源代码中,

ENTRY(entry_INT80_compat)
定义
int 0x80
。32位和64位进程在执行时都使用相同的入口点
entry_64.S

syscall
定义了64位内核的本机入口点,其中包括中断/错误处理程序以及
entry_64_compat.S
来自长模式(又名64位模式)进程的本机系统调用。

int0x80
定义了从compat模式到64位内核的系统调用入口点,以及
sysenter
在64位进程中的特殊情况。(
$__USER32_CS
在64位进程中也可能会到达该入口点,但它会推送
syscall
,因此它将始终以32位模式返回。)该可能的用例指令有32位版本,在AMD
CPU上受支持,Linux支持对于来自32位进程的快速32位系统调用也是如此。

我想一个

int 0x80
自定义代码段的描述符在64位模式是,如果你想使用一个
modify_ldt
,你安装
int0x80

iret
推送段自身进行注册
int0x80
,Linux总是
iret
通过调用从系统调用返回
syscall
。64位
pt_regs->cs
入口点设置
->ss
__USER_CS
常量,
__USER_DS
以及
entry_32.S
。(SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

int 0x80
在32位内核中定义入口点,并且完全不涉及。

entry_64_compat.S
在入口点的Linux
4.12的
 ENTRY(entry_INT80_compat) ...  (see the github URL for the full source)

structpt_regs

代码将eax零扩展为rax,然后将所有寄存器压入内核堆栈以形成a

ptrace
。这是从系统调用返回时恢复的位置。它采用已保存用户空间寄存器(用于任何入口点)的标准布局,因此
strace
,如果其他进程(例如gdb或
ptrace
ptrace
在系统调用内使用该进程,则会从该进程读取和/或写入该内存。(
sysenter
修改寄存器是使其他入口点的返回路径变得复杂的一件事。请参见注释。)

但是它推送

syscall32
而不是r8 / r9 / r10 / r11。(
call*ia32_sys_call_table(, %rax,8)
并且AMD
rbx
入口点为r8-r15存储零。)

我认为r8-r11的调零与历史行为相符。在为所有compat
syscall设置完整的pt_regs之前,入口点仅保存了C调用密集的寄存器。它直接从ASM出动

rbp
,并且这些功能遵循调用约定,所以他们保留
rsp
r12-r15
r8-r11
,和
ptrace
。调零
ebx
而不是让它们保持未定义状态可能是避免内核泄漏信息的一种方法。
ecx
如果用户空间的调用保留寄存器的唯一副本位于C函数将其保存的内核堆栈上,则IDK如何处理。我怀疑它使用堆栈展开元数据在此处找到它们。

当前的实现(Linux的4.12)调度从C
32位ABI系统调用,重新加载保存的

pt_regs
mov %r10,%rcx
等从
syscall
。(64位本机系统调用直接从asm调度,只
sysret
需要考虑函数和之间的调用约定之间的细微差别
syscall
。不幸的是,它不能总是使用
int 0x80
,因为CPU错误使它对非规范地址不安全。它确实会尝试,因此快速路径确实非常快,尽管
do_syscall_32_irqs_on(structpt_regs*regs)
它本身仍需要数十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括

ia32_sys_call_table
从64位开始)最终以结束
ia32
。它分派给
arch/x86/entry/common.c
具有6个零扩展args
的函数指针。这样可以避免在更多情况下需要围绕64位本机syscall函数进行包装以保留该行为,因此更多
if (likely(nr < IA32_NR_syscalls)) {    regs->ax = ia32_sys_call_table[nr](      (unsigned int)regs->bx, (unsigned int)regs->cx,      (unsigned int)regs->dx, (unsigned int)regs->si,      (unsigned int)regs->di, (unsigned int)regs->bp);}syscall_return_slowpath(regs);
表条目可以直接作为本机系统调用实现。

Linux 4.12

mov

xchg

在从asm分派32位系统调用的旧版Linux中(就像64位仍然如此),int80入口点本身使用32位寄存器将args

mov%edx,%edx
和和
sysenter
指令放入正确的寄存器中。它甚至用于
syscall32
将EDX零扩展为RDX(因为arg3在两种约定中都使用相同的寄存器)。
这里的代码。该代码在
write()
int0x80
入口点重复。


简单的例子/测试程序:

我编写了一个简单的Hello World(使用NASM语法),该寄存器将所有寄存器设置为非零上半部分,然后使用进行两次

.rodata
系统调用
-EFAULT
,其中一个使用指向
syscall
(成功的)字符串的指针,第二个使用指向堆栈的指针(失败
write()
)。

然后,它将本机64位

int 0x80
ABI用于
lea
堆栈中的字符(64位指针),然后再次退出。

因此,所有这些示例都正确地使用了ABI,除了第二个

mov
尝试传递一个64位指针并将其截断之外。

如果将其构建为与位置无关的可执行文件,则第一个也会失败。(您必须使用相对于RIP
的地址,

hello:
而不是
gdbgui
将其地址保存
;;;
到寄存器中。)

我使用了gdb,但使用了您喜欢的任何调试器。使用一个突出显示自上一步以来已更改的寄存器的寄存器。

global _start_start:    mov  rax, 0x123456789abcdef    mov  rbx, rax    mov  rcx, rax    mov  rdx, rax    mov  rsi, rax    mov  rdi, rax    mov  rbp, rax    mov  r8, rax    mov  r9, rax    mov  r10, rax    mov  r11, rax    mov  r12, rax    mov  r13, rax    mov  r14, rax    mov  r15, rax    ;; 32-bit ABI    mov  rax, 0xffffffff00000004          ; high garbage + __NR_write (unistd_32.h)    mov  rbx, 0xffffffff00000001          ; high garbage + fd=1    mov  rcx, 0xffffffff00000000 + .hello    mov  rdx, 0xffffffff00000000 + .hellolen    ;stdafter_setup:       ; set a breakpoint here    int  0x80        ; write(1, hello, hellolen);   32-bit ABI    ;; succeeds, writing to stdout;;; changes to registers:   r8-r11 = 0.  rax=14 = return value    ; ebx still = 1 = STDOUT_FILENO    push 'bye' + (0xa<<(3*8))    mov  rcx, rsp    ; rcx = 64-bit pointer that won't work if truncated    mov  edx, 4    mov  eax, 4      ; __NR_write (unistd_32.h)    int  0x80        ; write(ebx=1, ecx=truncated pointer,  edx=4);  32-bit    ;; fails, nothing printed;;; changes to registers: rax=-14 = -EFAULT  (from /usr/include/asm-generic/errno-base.h)    mov  r10, rax    ; save return value as exit status    mov  r8, r15    mov  r9, r15    mov  r11, r15    ; make these regs non-zero again    ;; 64-bit ABI    mov  eax, 1      ; __NR_write (unistd_64.h)    mov  edi, 1    mov  rsi, rsp    mov  edx, 4    syscall          ; write(edi=1, rsi='byen' on the stack,  rdx=4);  64-bit    ;; succeeds: writes to stdout and returns 4 in rax;;; changes to registers: rax=4 = length return value;;; rcx = 0x400112 = RIP.   r11 = 0x302 = eflags with an extra bit set.;;; (This is not a coincidence, it's how sysret works.  But don't depend on it, since iret could leave something else)    mov  edi, r10d    ;xor  edi,edi    mov  eax, 60     ; __NR_exit (unistd_64.h)    syscall          ; _exit(edi = first int 0x80 result);  64-bit    ;; succeeds, exit status = low byte of first int 0x80 result = 14section .rodata_start.hello:    db "Hello World!", 0xa, 0_start.hellolen  equ   $ - _start.hello
对于调试asm源代码效果很好,但对于反汇编而言效果不佳。尽管如此,它确实具有一个至少适用于整数reg的寄存器窗格,并且在此示例中效果很好。

请参阅内联

yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asmld -o abi32-from-64 abi32-from-64.o
注释,描述系统调用如何更改寄存器

gdb ./abi32-from-64

使用以下命令将其构建为64位静态二进制文件

gdb

运行

set disassembly-flavor intel
。在
layoutreg
中运行
~/.gdbinit
.intel_syntax
如果还没有,请运行
(gdb)  set disassembly-flavor intel(gdb)  layout reg(gdb)  b  after_setup(gdb)  r(gdb)  si          # step instruction    press return to repeat the last command, keep stepping
。(GAS
[+++]就像MASM,而不是NASM,但是它们足够接近,如果您喜欢NASM语法,则很容易阅读。)

[+++]

当gdb的TUI模式混乱时,按Ctrl-L。即使程序无法自行打印输出,这种情况也很容易发生。



)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
Error[8]: Undefined offset: 141, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况?

TL:DR

int 0x80
只要正确使用任何指针,只要任何指针都适合32位( 堆栈指针不适合 ), 它就可以工作 。同样,将其
strace

解码错误
,将寄存器内容当作64位
syscall
ABI进行解码。(到目前为止,还没有一种简单/可靠的方法
strace
可以告诉您。)

int 0x80
将r8-r11归零,并保留其他所有内容。就像在32位代码中使用32位电话号码一样使用它。(或者更好,不要使用它!)

并非所有系统都支持

int 0x80
:Linux的Windows子系统(WSL)严格来说仅是64位:
int0x80
根本不起作用。也可以在没有IA-32仿真的情况下构建Linux内核。(不支持32位可执行文件,不支持32位系统调用)。


详细信息:保存/恢复了什么,内核使用了哪些部分

int 0x80
使用
eax
(不是完整的
rax
)作为系统调用号,调度到32位用户空间
int0x80
使用的同一函数指针表。(这些指针指向
sys_whatever
内核内部本机64位实现的实现或包装。系统调用实际上是跨越用户/内核边界的函数调用。)

仅传递arg寄存器的低32位。 的上半部分

rbx
-
rbp
被保留,但忽略了
int 0x80
系统调用。

请注意,将错误的指针传递给系统调用不会导致SIGSEGV。而是系统调用返回
-EFAULT
。如果您不检查错误返回值(使用调试器或跟踪工具),它将似乎无提示地失败。

除了 r8-r11被清零 之外,所有寄存器(当然是eax除外)都被保存/恢复(包括RFLAGS和整数regs的高32位)。

r12-r15
在x86-64 SysV ABI的函数调用约定中保留了调用,因此
int0x80
64位清零的寄存器是AMD64添加的“新”寄存器的调用集中子集。

通过对内核内部实现寄存器保存的方式进行一些内部更改,保留了此行为,并且内核中的注释提到它可以在64位上使用,因此此ABI可能是稳定的。(即,您可以指望r8-r11被清零,并且所有其他内容都将保留。)

返回值被符号扩展以填充64位

rax

(Linux将32位sys_函数声明为返回有符号的
long
。)这意味着
void*mmap()
在64位寻址模式下使用之前,指针返回值(如from )需要零扩展。

与不同

sysenter
,它保留的原始值
cs
,因此它以与调用时相同的方式返回到用户空间。(使用
sysenter
结果将内核设置
cs
$__USER32_CS
,这将为32位代码段选择一个描述符。)


strace``int 0x80
为64位进程 解码 不正确 。它将好像处理已使用
syscall
而不是进行解码
int0x80
。 这可能非常令人困惑。例如因为
strace
打印的
write(0, NULL, 12 <unfinished ... exitstatus 1>
eax=1
/
int 
_exit(ebx)
x80
,实际上
write(rdi, rsi, rdx)
不是
int 0x80


0x00000000
只要所有参数(包括指针)都适合寄存器的低32位,它就起作用。x86-64 SysV
ABI中默认代码模型(“小”)中的静态代码和数据就是这种情况。(第3.5.1:
所有的符号被称为是位于该范围内的虚拟地址
0x7effffff
mov edi,hello
,所以你可以做一样的东西
mov $hello, %edi
(AT&T 不是),以获得一个指针与5个字节的指令寄存器)。

但是, 这是

gcc
对的情况下与位置无关的可执行文件,其中许多Linux发行版现在配置
hello.c
默认情况下,使
(他们启用ASLR的可执行文件)。例如,我
puts
在Arch
Linux上编译了一个,并在main的开始处设置了一个断点。传递给的字符串常量
0x555555554724
为at
write
,因此32位ABI
rsp
系统调用将不起作用。(GDB默认情况下会禁用ASLR,因此,如果您从GDB内部运行,则每次运行时总是看到相同的地址。)

Linux将堆栈放在规范地址的上限和下限之间的“间隙”附近,即,堆栈的顶部为2 ^
48-1。(或者是随机的,启用了ASLR)。因此,

_start
在进入
0x7fffffffe550
典型的静态链接可执行文件时
esp
,具体取决于env
vars和args的大小。截断此指针
-EFAULT
不会指向任何有效的内存,因此,
rsp
如果您尝试传递截断的堆栈指针,则通常会返回带有指针输入的系统调用。(如果你截断你的程序会崩溃
esp
arch/x86/entry/entry_64_compat.S
,然后做与堆栈,例如,如果你建立的32位汇编源作为64位可执行任何东西。)


它如何在内核中工作:

在Linux源代码中,

ENTRY(entry_INT80_compat)
定义
int 0x80
。32位和64位进程在执行时都使用相同的入口点
entry_64.S

syscall
定义了64位内核的本机入口点,其中包括中断/错误处理程序以及
entry_64_compat.S
来自长模式(又名64位模式)进程的本机系统调用。

int0x80
定义了从compat模式到64位内核的系统调用入口点,以及
sysenter
在64位进程中的特殊情况。(
$__USER32_CS
在64位进程中也可能会到达该入口点,但它会推送
syscall
,因此它将始终以32位模式返回。)该可能的用例指令有32位版本,在AMD
CPU上受支持,Linux支持对于来自32位进程的快速32位系统调用也是如此。

我想一个

int 0x80
自定义代码段的描述符在64位模式是,如果你想使用一个
modify_ldt
,你安装
int0x80

iret
推送段自身进行注册
int0x80
,Linux总是
iret
通过调用从系统调用返回
syscall
。64位
pt_regs->cs
入口点设置
->ss
__USER_CS
常量,
__USER_DS
以及
entry_32.S
。(SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

int 0x80
在32位内核中定义入口点,并且完全不涉及。

entry_64_compat.S
在入口点的Linux
4.12的
 ENTRY(entry_INT80_compat) ...  (see the github URL for the full source)

structpt_regs

代码将eax零扩展为rax,然后将所有寄存器压入内核堆栈以形成a

ptrace
。这是从系统调用返回时恢复的位置。它采用已保存用户空间寄存器(用于任何入口点)的标准布局,因此
strace
,如果其他进程(例如gdb或
ptrace
ptrace
在系统调用内使用该进程,则会从该进程读取和/或写入该内存。(
sysenter
修改寄存器是使其他入口点的返回路径变得复杂的一件事。请参见注释。)

但是它推送

syscall32
而不是r8 / r9 / r10 / r11。(
call*ia32_sys_call_table(, %rax,8)
并且AMD
rbx
入口点为r8-r15存储零。)

我认为r8-r11的调零与历史行为相符。在为所有compat
syscall设置完整的pt_regs之前,入口点仅保存了C调用密集的寄存器。它直接从ASM出动

rbp
,并且这些功能遵循调用约定,所以他们保留
rsp
r12-r15
r8-r11
,和
ptrace
。调零
ebx
而不是让它们保持未定义状态可能是避免内核泄漏信息的一种方法。
ecx
如果用户空间的调用保留寄存器的唯一副本位于C函数将其保存的内核堆栈上,则IDK如何处理。我怀疑它使用堆栈展开元数据在此处找到它们。

当前的实现(Linux的4.12)调度从C
32位ABI系统调用,重新加载保存的

pt_regs
mov %r10,%rcx
等从
syscall
。(64位本机系统调用直接从asm调度,只
sysret
需要考虑函数和之间的调用约定之间的细微差别
syscall
。不幸的是,它不能总是使用
int 0x80
,因为CPU错误使它对非规范地址不安全。它确实会尝试,因此快速路径确实非常快,尽管
do_syscall_32_irqs_on(structpt_regs*regs)
它本身仍需要数十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括

ia32_sys_call_table
从64位开始)最终以结束
ia32
。它分派给
arch/x86/entry/common.c
具有6个零扩展args
的函数指针。这样可以避免在更多情况下需要围绕64位本机syscall函数进行包装以保留该行为,因此更多
if (likely(nr < IA32_NR_syscalls)) {    regs->ax = ia32_sys_call_table[nr](      (unsigned int)regs->bx, (unsigned int)regs->cx,      (unsigned int)regs->dx, (unsigned int)regs->si,      (unsigned int)regs->di, (unsigned int)regs->bp);}syscall_return_slowpath(regs);
表条目可以直接作为本机系统调用实现。

Linux 4.12

mov

xchg

在从asm分派32位系统调用的旧版Linux中(就像64位仍然如此),int80入口点本身使用32位寄存器将args

mov%edx,%edx
和和
sysenter
指令放入正确的寄存器中。它甚至用于
syscall32
将EDX零扩展为RDX(因为arg3在两种约定中都使用相同的寄存器)。
这里的代码。该代码在
write()
int0x80
入口点重复。


简单的例子/测试程序:

我编写了一个简单的Hello World(使用NASM语法),该寄存器将所有寄存器设置为非零上半部分,然后使用进行两次

.rodata
系统调用
-EFAULT
,其中一个使用指向
syscall
(成功的)字符串的指针,第二个使用指向堆栈的指针(失败
write()
)。

然后,它将本机64位

int 0x80
ABI用于
lea
堆栈中的字符(64位指针),然后再次退出。

因此,所有这些示例都正确地使用了ABI,除了第二个

mov
尝试传递一个64位指针并将其截断之外。

如果将其构建为与位置无关的可执行文件,则第一个也会失败。(您必须使用相对于RIP
的地址,

hello:
而不是
gdbgui
将其地址保存
;;;
到寄存器中。)

我使用了gdb,但使用了您喜欢的任何调试器。使用一个突出显示自上一步以来已更改的寄存器的寄存器。

global _start_start:    mov  rax, 0x123456789abcdef    mov  rbx, rax    mov  rcx, rax    mov  rdx, rax    mov  rsi, rax    mov  rdi, rax    mov  rbp, rax    mov  r8, rax    mov  r9, rax    mov  r10, rax    mov  r11, rax    mov  r12, rax    mov  r13, rax    mov  r14, rax    mov  r15, rax    ;; 32-bit ABI    mov  rax, 0xffffffff00000004          ; high garbage + __NR_write (unistd_32.h)    mov  rbx, 0xffffffff00000001          ; high garbage + fd=1    mov  rcx, 0xffffffff00000000 + .hello    mov  rdx, 0xffffffff00000000 + .hellolen    ;stdafter_setup:       ; set a breakpoint here    int  0x80        ; write(1, hello, hellolen);   32-bit ABI    ;; succeeds, writing to stdout;;; changes to registers:   r8-r11 = 0.  rax=14 = return value    ; ebx still = 1 = STDOUT_FILENO    push 'bye' + (0xa<<(3*8))    mov  rcx, rsp    ; rcx = 64-bit pointer that won't work if truncated    mov  edx, 4    mov  eax, 4      ; __NR_write (unistd_32.h)    int  0x80        ; write(ebx=1, ecx=truncated pointer,  edx=4);  32-bit    ;; fails, nothing printed;;; changes to registers: rax=-14 = -EFAULT  (from /usr/include/asm-generic/errno-base.h)    mov  r10, rax    ; save return value as exit status    mov  r8, r15    mov  r9, r15    mov  r11, r15    ; make these regs non-zero again    ;; 64-bit ABI    mov  eax, 1      ; __NR_write (unistd_64.h)    mov  edi, 1    mov  rsi, rsp    mov  edx, 4    syscall          ; write(edi=1, rsi='byen' on the stack,  rdx=4);  64-bit    ;; succeeds: writes to stdout and returns 4 in rax;;; changes to registers: rax=4 = length return value;;; rcx = 0x400112 = RIP.   r11 = 0x302 = eflags with an extra bit set.;;; (This is not a coincidence, it's how sysret works.  But don't depend on it, since iret could leave something else)    mov  edi, r10d    ;xor  edi,edi    mov  eax, 60     ; __NR_exit (unistd_64.h)    syscall          ; _exit(edi = first int 0x80 result);  64-bit    ;; succeeds, exit status = low byte of first int 0x80 result = 14section .rodata_start.hello:    db "Hello World!", 0xa, 0_start.hellolen  equ   $ - _start.hello
对于调试asm源代码效果很好,但对于反汇编而言效果不佳。尽管如此,它确实具有一个至少适用于整数reg的寄存器窗格,并且在此示例中效果很好。

请参阅内联

yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asmld -o abi32-from-64 abi32-from-64.o
注释,描述系统调用如何更改寄存器

gdb ./abi32-from-64

使用以下命令将其构建为64位静态二进制文件

gdb

运行

set disassembly-flavor intel
。在
layoutreg
中运行
~/.gdbinit
.intel_syntax
如果还没有,请运行
(gdb)  set disassembly-flavor intel(gdb)  layout reg(gdb)  b  after_setup(gdb)  r(gdb)  si          # step instruction    press return to repeat the last command, keep stepping
。(GAS
就像MASM,而不是NASM,但是它们足够接近,如果您喜欢NASM语法,则很容易阅读。)

[+++]

当gdb的TUI模式混乱时,按Ctrl-L。即使程序无法自行打印输出,这种情况也很容易发生。



)
File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 165, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
如果以64位代码使用32位int 0&#120;80 Linux ABI,会发生什么情况?_随笔_内存溢出

如果以64位代码使用32位int 0&#120;80 Linux ABI,会发生什么情况?

如果以64位代码使用32位int 0&#120;80 Linux ABI,会发生什么情况?,第1张

如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况?

TL:DR

int 0x80
只要正确使用任何指针,只要任何指针都适合32位( 堆栈指针不适合 ), 它就可以工作 。同样,将其
strace

解码错误
,将寄存器内容当作64位
syscall
ABI进行解码。(到目前为止,还没有一种简单/可靠的方法
strace
可以告诉您。)

int 0x80
将r8-r11归零,并保留其他所有内容。就像在32位代码中使用32位电话号码一样使用它。(或者更好,不要使用它!)

并非所有系统都支持

int 0x80
:Linux的Windows子系统(WSL)严格来说仅是64位:
int0x80
根本不起作用。也可以在没有IA-32仿真的情况下构建Linux内核。(不支持32位可执行文件,不支持32位系统调用)。


详细信息:保存/恢复了什么,内核使用了哪些部分

int 0x80
使用
eax
(不是完整的
rax
)作为系统调用号,调度到32位用户空间
int0x80
使用的同一函数指针表。(这些指针指向
sys_whatever
内核内部本机64位实现的实现或包装。系统调用实际上是跨越用户/内核边界的函数调用。)

仅传递arg寄存器的低32位。 的上半部分

rbx
-
rbp
被保留,但忽略了
int 0x80
系统调用。

请注意,将错误的指针传递给系统调用不会导致SIGSEGV。而是系统调用返回
-EFAULT
。如果您不检查错误返回值(使用调试器或跟踪工具),它将似乎无提示地失败。

除了 r8-r11被清零 之外,所有寄存器(当然是eax除外)都被保存/恢复(包括RFLAGS和整数regs的高32位)。

r12-r15
在x86-64 SysV ABI的函数调用约定中保留了调用,因此
int0x80
64位清零的寄存器是AMD64添加的“新”寄存器的调用集中子集。

通过对内核内部实现寄存器保存的方式进行一些内部更改,保留了此行为,并且内核中的注释提到它可以在64位上使用,因此此ABI可能是稳定的。(即,您可以指望r8-r11被清零,并且所有其他内容都将保留。)

返回值被符号扩展以填充64位

rax

(Linux将32位sys_函数声明为返回有符号的
long
。)这意味着
void*mmap()
在64位寻址模式下使用之前,指针返回值(如from )需要零扩展。

与不同

sysenter
,它保留的原始值
cs
,因此它以与调用时相同的方式返回到用户空间。(使用
sysenter
结果将内核设置
cs
$__USER32_CS
,这将为32位代码段选择一个描述符。)


strace``int 0x80
为64位进程 解码 不正确 。它将好像处理已使用
syscall
而不是进行解码
int0x80
。 这可能非常令人困惑。例如因为
strace
打印的
write(0, NULL, 12 <unfinished ... exitstatus 1>
eax=1
/
int 
_exit(ebx)
x80
,实际上
write(rdi, rsi, rdx)
不是
int 0x80


0x00000000
只要所有参数(包括指针)都适合寄存器的低32位,它就起作用。x86-64 SysV
ABI中默认代码模型(“小”)中的静态代码和数据就是这种情况。(第3.5.1:
所有的符号被称为是位于该范围内的虚拟地址
0x7effffff
mov edi,hello
,所以你可以做一样的东西
mov $hello, %edi
(AT&T 不是),以获得一个指针与5个字节的指令寄存器)。

但是, 这是

gcc
对的情况下与位置无关的可执行文件,其中许多Linux发行版现在配置
hello.c
默认情况下,使
(他们启用ASLR的可执行文件)。例如,我
puts
在Arch
Linux上编译了一个,并在main的开始处设置了一个断点。传递给的字符串常量
0x555555554724
为at
write
,因此32位ABI
rsp
系统调用将不起作用。(GDB默认情况下会禁用ASLR,因此,如果您从GDB内部运行,则每次运行时总是看到相同的地址。)

Linux将堆栈放在规范地址的上限和下限之间的“间隙”附近,即,堆栈的顶部为2 ^
48-1。(或者是随机的,启用了ASLR)。因此,

_start
在进入
0x7fffffffe550
典型的静态链接可执行文件时
esp
,具体取决于env
vars和args的大小。截断此指针
-EFAULT
不会指向任何有效的内存,因此,
rsp
如果您尝试传递截断的堆栈指针,则通常会返回带有指针输入的系统调用。(如果你截断你的程序会崩溃
esp
arch/x86/entry/entry_64_compat.S
,然后做与堆栈,例如,如果你建立的32位汇编源作为64位可执行任何东西。)


它如何在内核中工作:

在Linux源代码中,

ENTRY(entry_INT80_compat)
定义
int 0x80
。32位和64位进程在执行时都使用相同的入口点
entry_64.S

syscall
定义了64位内核的本机入口点,其中包括中断/错误处理程序以及
entry_64_compat.S
来自长模式(又名64位模式)进程的本机系统调用。

int0x80
定义了从compat模式到64位内核的系统调用入口点,以及
sysenter
在64位进程中的特殊情况。(
$__USER32_CS
在64位进程中也可能会到达该入口点,但它会推送
syscall
,因此它将始终以32位模式返回。)该可能的用例指令有32位版本,在AMD
CPU上受支持,Linux支持对于来自32位进程的快速32位系统调用也是如此。

我想一个

int 0x80
自定义代码段的描述符在64位模式是,如果你想使用一个
modify_ldt
,你安装
int0x80

iret
推送段自身进行注册
int0x80
,Linux总是
iret
通过调用从系统调用返回
syscall
。64位
pt_regs->cs
入口点设置
->ss
__USER_CS
常量,
__USER_DS
以及
entry_32.S
。(SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

int 0x80
在32位内核中定义入口点,并且完全不涉及。

entry_64_compat.S
在入口点的Linux
4.12的
 ENTRY(entry_INT80_compat) ...  (see the github URL for the full source)

structpt_regs

代码将eax零扩展为rax,然后将所有寄存器压入内核堆栈以形成a

ptrace
。这是从系统调用返回时恢复的位置。它采用已保存用户空间寄存器(用于任何入口点)的标准布局,因此
strace
,如果其他进程(例如gdb或
ptrace
ptrace
在系统调用内使用该进程,则会从该进程读取和/或写入该内存。(
sysenter
修改寄存器是使其他入口点的返回路径变得复杂的一件事。请参见注释。)

但是它推送

syscall32
而不是r8 / r9 / r10 / r11。(
call*ia32_sys_call_table(, %rax,8)
并且AMD
rbx
入口点为r8-r15存储零。)

我认为r8-r11的调零与历史行为相符。在为所有compat
syscall设置完整的pt_regs之前,入口点仅保存了C调用密集的寄存器。它直接从ASM出动

rbp
,并且这些功能遵循调用约定,所以他们保留
rsp
r12-r15
r8-r11
,和
ptrace
。调零
ebx
而不是让它们保持未定义状态可能是避免内核泄漏信息的一种方法。
ecx
如果用户空间的调用保留寄存器的唯一副本位于C函数将其保存的内核堆栈上,则IDK如何处理。我怀疑它使用堆栈展开元数据在此处找到它们。

当前的实现(Linux的4.12)调度从C
32位ABI系统调用,重新加载保存的

pt_regs
mov %r10,%rcx
等从
syscall
。(64位本机系统调用直接从asm调度,只
sysret
需要考虑函数和之间的调用约定之间的细微差别
syscall
。不幸的是,它不能总是使用
int 0x80
,因为CPU错误使它对非规范地址不安全。它确实会尝试,因此快速路径确实非常快,尽管
do_syscall_32_irqs_on(structpt_regs*regs)
它本身仍需要数十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括

ia32_sys_call_table
从64位开始)最终以结束
ia32
。它分派给
arch/x86/entry/common.c
具有6个零扩展args
的函数指针。这样可以避免在更多情况下需要围绕64位本机syscall函数进行包装以保留该行为,因此更多
if (likely(nr < IA32_NR_syscalls)) {    regs->ax = ia32_sys_call_table[nr](      (unsigned int)regs->bx, (unsigned int)regs->cx,      (unsigned int)regs->dx, (unsigned int)regs->si,      (unsigned int)regs->di, (unsigned int)regs->bp);}syscall_return_slowpath(regs);
表条目可以直接作为本机系统调用实现。

Linux 4.12

mov

xchg

在从asm分派32位系统调用的旧版Linux中(就像64位仍然如此),int80入口点本身使用32位寄存器将args

mov%edx,%edx
和和
sysenter
指令放入正确的寄存器中。它甚至用于
syscall32
将EDX零扩展为RDX(因为arg3在两种约定中都使用相同的寄存器)。
这里的代码。该代码在
write()
int0x80
入口点重复。


简单的例子/测试程序:

我编写了一个简单的Hello World(使用NASM语法),该寄存器将所有寄存器设置为非零上半部分,然后使用进行两次

.rodata
系统调用
-EFAULT
,其中一个使用指向
syscall
(成功的)字符串的指针,第二个使用指向堆栈的指针(失败
write()
)。

然后,它将本机64位

int 0x80
ABI用于
lea
堆栈中的字符(64位指针),然后再次退出。

因此,所有这些示例都正确地使用了ABI,除了第二个

mov
尝试传递一个64位指针并将其截断之外。

如果将其构建为与位置无关的可执行文件,则第一个也会失败。(您必须使用相对于RIP
的地址,

hello:
而不是
gdbgui
将其地址保存
;;;
到寄存器中。)

我使用了gdb,但使用了您喜欢的任何调试器。使用一个突出显示自上一步以来已更改的寄存器的寄存器。

global _start_start:    mov  rax, 0x123456789abcdef    mov  rbx, rax    mov  rcx, rax    mov  rdx, rax    mov  rsi, rax    mov  rdi, rax    mov  rbp, rax    mov  r8, rax    mov  r9, rax    mov  r10, rax    mov  r11, rax    mov  r12, rax    mov  r13, rax    mov  r14, rax    mov  r15, rax    ;; 32-bit ABI    mov  rax, 0xffffffff00000004          ; high garbage + __NR_write (unistd_32.h)    mov  rbx, 0xffffffff00000001          ; high garbage + fd=1    mov  rcx, 0xffffffff00000000 + .hello    mov  rdx, 0xffffffff00000000 + .hellolen    ;stdafter_setup:       ; set a breakpoint here    int  0x80        ; write(1, hello, hellolen);   32-bit ABI    ;; succeeds, writing to stdout;;; changes to registers:   r8-r11 = 0.  rax=14 = return value    ; ebx still = 1 = STDOUT_FILENO    push 'bye' + (0xa<<(3*8))    mov  rcx, rsp    ; rcx = 64-bit pointer that won't work if truncated    mov  edx, 4    mov  eax, 4      ; __NR_write (unistd_32.h)    int  0x80        ; write(ebx=1, ecx=truncated pointer,  edx=4);  32-bit    ;; fails, nothing printed;;; changes to registers: rax=-14 = -EFAULT  (from /usr/include/asm-generic/errno-base.h)    mov  r10, rax    ; save return value as exit status    mov  r8, r15    mov  r9, r15    mov  r11, r15    ; make these regs non-zero again    ;; 64-bit ABI    mov  eax, 1      ; __NR_write (unistd_64.h)    mov  edi, 1    mov  rsi, rsp    mov  edx, 4    syscall          ; write(edi=1, rsi='byen' on the stack,  rdx=4);  64-bit    ;; succeeds: writes to stdout and returns 4 in rax;;; changes to registers: rax=4 = length return value;;; rcx = 0x400112 = RIP.   r11 = 0x302 = eflags with an extra bit set.;;; (This is not a coincidence, it's how sysret works.  But don't depend on it, since iret could leave something else)    mov  edi, r10d    ;xor  edi,edi    mov  eax, 60     ; __NR_exit (unistd_64.h)    syscall          ; _exit(edi = first int 0x80 result);  64-bit    ;; succeeds, exit status = low byte of first int 0x80 result = 14section .rodata_start.hello:    db "Hello World!", 0xa, 0_start.hellolen  equ   $ - _start.hello
对于调试asm源代码效果很好,但对于反汇编而言效果不佳。尽管如此,它确实具有一个至少适用于整数reg的寄存器窗格,并且在此示例中效果很好。

请参阅内联

yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asmld -o abi32-from-64 abi32-from-64.o
注释,描述系统调用如何更改寄存器

gdb ./abi32-from-64

使用以下命令将其构建为64位静态二进制文件

gdb

运行

set disassembly-flavor intel
。在
layoutreg
中运行
~/.gdbinit
.intel_syntax
如果还没有,请运行
(gdb)  set disassembly-flavor intel(gdb)  layout reg(gdb)  b  after_setup(gdb)  r(gdb)  si          # step instruction    press return to repeat the last command, keep stepping
。(GAS
就像MASM,而不是NASM,但是它们足够接近,如果您喜欢NASM语法,则很容易阅读。)

当gdb的TUI模式混乱时,按Ctrl-L。即使程序无法自行打印输出,这种情况也很容易发生。



欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4976517.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-13
下一篇 2022-11-14

发表评论

登录后才能评论

评论列表(0条)

保存