为什么ptrace显示32位execve系统调用的EAX是64位调用编号59?在x86-64上如何工作32位系统调用?

3
我在使用以下代码玩弄 ptrace,发现即使我使用了 -m32 选项编译,execve 的系统调用号却仍然为59。由于我正在64位机器上使用Ubuntu,这是可以理解的。
很快就产生了一个问题:“在32位机和64位机上,libc32 的行为是否有所不同?它们有区别吗?”因此我查看了64位中的 libc32。 然而,libc 中 execve 系统调用号为11,与32位系统下的 execv 系统调用号完全相同。那么魔术发生在哪里?谢谢提前回答。
以下是代码,源自https://www.linuxjournal.com/article/6100
#include <sys/ptrace.h>                                                                                                           
#include <sys/types.h>                                                                                                            
#include <sys/wait.h>                                                                                                             
#include <unistd.h>                                                                                                               
#include <sys/user.h>                                                                                                             
#include <stdio.h>                                                                                                                
                                                                                                                                  
int main()                                                                                                                        
{                                                                                                                                 
        pid_t child;                                                                                                              
        long  orig_eax;                                                                                                           
        child = fork();                                                                                                           
        if (child == 0) {                                                                                                         
                ptrace(PTRACE_TRACEME, 0, NULL, NULL);                                                                            
                execl("/bin/ls", "ls", NULL);                                                                                     
        } else {                                                                                                                  
                wait(NULL);                                                                                                       
                orig_eax = ptrace(PTRACE_PEEKUSER,                                                                                
#ifdef __x86_64__                                                                                                                 
                                child, &((struct user_regs_struct *)0)->orig_rax,                                                 
#else                                                                                                                             
                                child, &((struct user_regs_struct *)0)->orig_eax,                                                 
#endif                                                                                                                            
                                NULL);                                                                                            
                printf("The child made a "                                                                                       
                        "system call %ld\n", orig_eax);                                                                           
                ptrace (PTRACE_CONT, child, NULL, NULL);                                                                          
        }                                                                                                                         
        return 0;                                                                                                                 
}

以下是代码的结果

~/my-sandbox/ptrace$ file s1 && ./s1
s1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f84894c2f5373051682858937bf54a66f21cbeb4, for GNU/Linux 3.2.0, not stripped
The child made a system call 59

~/my-sandbox/ptrace$ file s2 && ./s2
s2: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=cac6a2bbeee164e27c11764c1b68f4ddd06405cf, for GNU/Linux 3.2.0, with debug_info, not stripped
The child made a system call 59

这是我从32位可执行文件中使用gdb获取到的内容。正如您所看到的,它正在使用/lib/i386-linux-gnu/libc.so.6,并且execve系统调用号为11。
>>> bt
#0  0xf7e875a0 in execve () from /lib/i386-linux-gnu/libc.so.6
#1  0xf7e8799f in execl () from /lib/i386-linux-gnu/libc.so.6
#2  0x565562a4 in main () at simple1.c:15
>>> disassemble
Dump of assembler code for function execve:
=> 0xf7e875a0 <+0>: endbr32 
   0xf7e875a4 <+4>: push   %ebx
   0xf7e875a5 <+5>: mov    0x10(%esp),%edx
   0xf7e875a9 <+9>: mov    0xc(%esp),%ecx
   0xf7e875ad <+13>:    mov    0x8(%esp),%ebx
   0xf7e875b1 <+17>:    mov    $0xb,%eax
   0xf7e875b6 <+22>:    call   *%gs:0x10
   0xf7e875bd <+29>:    pop    %ebx
   0xf7e875be <+30>:    cmp    $0xfffff001,%eax
   0xf7e875c3 <+35>:    jae    0xf7dd9000
   0xf7e875c9 <+41>:    ret    
End of assembler dump.

1
libc使用内核导出的VDSO包装器,因此它可以在支持它的机器上受益于sysenter,而不是较慢的int $0x80。请参阅https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/以获取概述。 - Peter Cordes
正如您所看到的,32位libc execve包装器使用mov $0xb,%eax传递EAX=11。我不知道为什么您的程序在这种情况下找到了EAX=59。我肯定也希望在那里看到11,即来自unistd_32.h__NR_execve,特别是因为我们可以看到libc包装器正在传递它。与Windows WOW64不同,Linux上的32位用户空间直接调用内核,而不是远跳转到64位模式进行系统调用。因此,在64位系统上,32位用户空间库并不特殊;64位内核只是为32位进程提供了32位ABI。 - Peter Cordes
是的。我检查了32位机器上的libc32和64位机器上的libc32没有区别。我的问题是,尽管libc手动11(在具有32位elf的64位机器上)用于execve,但内核(它是64位的)如何将其视为59。 - dyjin
是的,我可以看到内核只能/应该为exeve保留59。那么在32位elf上,11和execve是如何转换为59的呢? - dyjin
也许在内核中,作为32位系统调用包装器的实现细节,在执行检查“PTRACE_TRACEME”的execve代码时,实际上会引发SIGTRAP? - Peter Cordes
1个回答

2
“execve”是特殊的,它是唯一一个与“PTRACE_TRACEME”有特殊交互的系统调用。在“strace”的工作方式中,其他系统调用会显示32位的调用号码。(而现代的“strace”需要特殊帮助来确定这是否是“int 0x80”/“sysenter”的32位调用号码,或者是64位调用号码,因为64位进程仍然可以调用“int 0x80”,尽管他们通常不应该这样做。这种支持是在2019年才添加的,使用了“PTRACE_GET_SYSCALL_INFO”。)
“当内核实际被调用时,EAX保存unistd_32.h中的__NR_execve的值为11。在glibc的execve包装器跳转到VDSO页面以通过此硬件支持的任何有效方法进入内核之前,它由mov $0xb,%eax设置(通常是sysenter)。但是,直到它到达主execve实现中检查PTRACE_TRACEME并引发SIGTRAP的某些代码之前,执行才会停止。显然,在那之前的某个时候,它调用了void set_personality_64bit(void)在arch/x86/kernel/process_64.c,其中包括”。
    /* Pretend that this comes from a 64bit execve */
    task_pt_regs(current)->orig_ax = __NR_execve;

我发现,在内核源代码浏览器中搜索__NR_execve,然后查看arch/x86中最可能的文件。我没有继续交叉引用以找到它是从哪里调用的;事实上它存在(并且假设设计合理、非混淆)非常强烈地指向这是你的谜题的答案。

@dyjin:如果一个回答完全回答了你的问题,你可以在投票箭头下方使用勾选标记“接受”该回答。 - Peter Cordes
是的,谢谢。对不起,我是新手。 - dyjin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接