O que acontece se você usar o int de 0x bits Linux ABI de 80 bits no código de 64 bits?

int 0x80 no Linux sempre invoca a ABI de 32 bits, independentemente de qual modo ela é chamada: args em ebx , ecx , … e syscall em /usr/include/asm/unistd_32.h . (Ou falha em kernels de 64 bits compilados sem CONFIG_IA32_EMULATION ).

O código de 64 bits deve usar syscall , com números de chamada de /usr/include/asm/unistd_64.h e args em rdi , rsi , etc. Consulte Quais são as convenções de chamada para chamadas de sistema UNIX e Linux em i386 e x86-64? . Se a sua pergunta foi marcada como uma duplicata disso, consulte esse link para obter detalhes sobre como você deve fazer chamadas do sistema no código de 32 ou 64 bits. Se você quiser entender exatamente o que aconteceu, continue lendo.


syscall chamadas do sistema syscall são mais rápidas do que as chamadas de sistema int 0x80 , portanto, use o syscall nativo de 64 bits, a menos que você esteja escrevendo um código de máquina poliglota que execute o mesmo quando executado como 32 ou 64 bits. (O sysenter sempre retorna no modo de 32 bits, portanto, não é útil no espaço do usuário de 64 bits, embora seja uma instrução x86-64 válida.)

Relacionados: O Guia Definitivo para Chamadas do Sistema Linux (em x86) para saber como fazer chamadas de sistema int 0x80 ou sysenter 32 bits ou chamadas de sistema syscall 64 bits ou chamar o vDSO para chamadas de sistema “virtuais” como gettimeofday . Mais informações sobre o que são as chamadas do sistema.


Usar o int 0x80 possibilita escrever algo que será montado no modo de 32 ou 64 bits, portanto, é útil para um exit_group() no final de um microbenchmark ou algo assim.

Os PDFs atuais dos documentos psABI System i386 e x86-64 oficiais que padronizam as convenções de chamada de function e syscall estão vinculados a partir de https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI .

Veja o wiki da tag x86 para guias iniciantes, manuais x86, documentação oficial e guias / resources de otimização de desempenho.


Mas como as pessoas continuam postando perguntas com código que usa o int 0x80 no código de 64 bits , ou acidentalmente construindo binários de 64 bits a partir do código-fonte escrito para 32 bits, eu me pergunto o que exatamente acontece no Linux atual?

O int 0x80 salva / restaura todos os registradores de 64 bits? Truncará quaisquer registros para 32 bits? O que acontece se você passar argumentos de ponteiro que tenham metades superiores diferentes de zero?

Isso funciona se você passar pointers de 32 bits?

TL: DR : int 0x80 funciona quando usado corretamente, contanto que qualquer pointers se encaixem em 32 bits ( pointers de pilha não cabem ). Além disso, strace decodifica errado , decodificando o conteúdo do registro como se fosse a ABI syscall 64 bits.

int 0x80 zeros r8-r11 e preserva todo o resto. Use-o exatamente como você faria no código de 32 bits, com os números de chamada de 32 bits. (Ou melhor, não use isso!)

Nem todos os sistemas suportam int 0x80 : O subsistema Windows Ubuntu é estritamente de 64 bits: int 0x80 não funciona . Também é possível construir kernels Linux sem a emulação IA-32 . (Sem suporte para executáveis ​​de 32 bits, sem suporte para chamadas de sistema de 32 bits).


Os detalhes: o que é salvo / restaurado, quais partes do que o kernel usa

int 0x80 usa eax (não o rax completo) como o número de chamada do sistema, despachando para a mesma tabela de pointers de function que usa o espaço de usuário de 32 bits int 0x80 . (Esses pointers são para sys_whatever implementações ou wrappers para a implementação nativa de 64 bits dentro do kernel. As chamadas do sistema são realmente chamadas de function através do limite do usuário / kernel.)

Apenas os 32 bits baixos de registros arg são passados. As metades superiores do rbxrbp são preservadas, mas ignoradas pelas chamadas do sistema int 0x80 . Note que passar um ponteiro ruim para uma chamada de sistema não resulta em SIGSEGV; em vez disso, a chamada do sistema retorna -EFAULT . Se você não verificar os valores de retorno de erro (com um depurador ou ferramenta de rastreamento), ele aparecerá para falhar silenciosamente.

Todos os registradores (exceto eax, é claro) são salvos / restaurados (incluindo RFLAGS, e os 32 superiores dos regs inteiros), exceto que r8-r11 são zerados . r12-r15 é preservado na convenção de chamada de function do x86-64 SysV ABI, portanto, os registradores que são zerados pelo int 0x80 em 64 bits são o subconjunto dos “novos” registros que o AMD64 adicionou.

Este comportamento foi preservado através de algumas mudanças internas em como o registro de economia foi implementado dentro do kernel, e comentários no kernel mencionam que é utilizável a partir de 64 bits, então esta ABI é provavelmente estável. (Ou seja, você pode contar com o r8-r11 sendo zerado e tudo o mais sendo preservado.)

O valor de retorno é estendido por sinal para preencher o rax 64 bits. (O Linux declara funções sys_ de 32 bits como retornando com sinal long .) Isso significa que valores de retorno de ponteiro (como de void *mmap() ) precisam ser estendidos de zero antes de serem usados ​​em modos de endereçamento de 64 bits

Ao contrário do sysenter , ele preserva o valor original de cs , portanto retorna ao espaço do usuário no mesmo modo em que foi chamado. (Usando os resultados do sysenter na configuração do kernel cs para $__USER32_CS , que seleciona um descritor para um 32-bit segmento de código.)


strace decodifica int 0x80 incorretamente para processos de 64 bits. Ele decodifica como se o processo tivesse usado syscall vez de int 0x80 . Isso pode ser muito confuso . por exemplo, desde que strace imprime write(0, NULL, 12 para eax=1 / int $0x80 , que é na verdade _exit(ebx) , não write(rdi, rsi, rdx) .


int 0x80 funciona contanto que todos os argumentos (incluindo pointers) caibam no baixo 32 de um registrador . Este é o caso do código estático e dos dados no modelo de código padrão (“small”) no x86-64 SysV ABI . (Seção 3.5.1: todos os símbolos são conhecidos por estarem localizados nos endereços virtuais no intervalo de 0x00000000 a 0x7effffff , então você pode fazer coisas como mov edi, hello (AT & T mov $hello, %edi ) para colocar um ponteiro em um registrador com uma instrução de 5 bytes).

Mas esse não é o caso dos executáveis ​​independentes de posição , que muitas distribuições Linux agora configuram o gcc para fazer por padrão (e habilitam o ASLR para executáveis). Por exemplo, eu compilei um hello.c no Arch Linux e defini um ponto de interrupção no início do main. A constante de string passada para puts estava em 0x555555554724 , portanto, uma chamada de sistema de write ABI de 32 bits não funcionaria. (O GDB desabilita o ASLR por padrão, assim você sempre verá o mesmo endereço de rodar para rodar, se você rodar dentro do GDB).

O Linux coloca a pilha perto do “intervalo” entre os intervalos superior e inferior dos endereços canônicos , isto é, com o topo da pilha em 2 ^ 48-1. (Ou em algum lugar random, com o ASLR ativado). Portanto, o rsp na input para _start em um executável com vinculação estática típica é algo como 0x7fffffffe550 , dependendo do tamanho de env vars e args. Truncar esse ponteiro para esp não aponta para qualquer memory válida, portanto, as chamadas do sistema com inputs de ponteiro normalmente retornarão -EFAULT se você tentar passar um ponteiro de pilha truncado. (E o seu programa irá travar se você truncar rsp para esp e então fazer qualquer coisa com a pilha, por exemplo, se você construiu uma fonte asm de 32 bits como um executável de 64 bits.)


Como funciona no kernel:

No código-fonte do Linux, arch/x86/entry/entry_64_compat.S define ENTRY(entry_INT80_compat) . Ambos os processos de 32 e 64 bits usam o mesmo ponto de input quando executam o int 0x80 .

entry_64.S define pontos de input nativos para um kernel de 64 bits, que inclui manipuladores de interrupção / falha e chamadas de sistema nativo syscall de processos de modo longo (também conhecido como modo de 64 bits) .

entry_64_compat.S define pontos de input de chamada de sistema do modo compat para um kernel de 64 bits, além do caso especial de int 0x80 em um processo de 64 bits. (O sysenter em um processo de 64 bits também pode ir para esse ponto de input, mas envia $__USER32_CS , portanto, ele sempre retornará no modo de 32 bits.) Há uma versão de 32 bits da instrução syscall , suportada em CPUs da AMD. e o Linux também suporta isso para chamadas de sistema rápidas de 32 bits a partir de processos de 32 bits.

Eu acho que um possível caso de uso para int 0x80 no modo de 64 bits é se você quiser usar um descritor de segmento de código personalizado que você instalou com modify_ldt . int 0x80 iret segment registra-se para uso com iret , e o Linux sempre retorna das chamadas de sistema int 0x80 via iret . O ponto de input pt_regs->cs 64 bits define pt_regs->cs e ->ss para constantes, __USER_CS e __USER_DS . (É normal que SS e DS usem os mesmos descritores de segmento. As diferenças de permissão são feitas com paginação, não segmentação.)

entry_32.S define pontos de input em um kernel de 32 bits e não está envolvido de forma alguma.

O ponto de input int 0x80 no entry_64_compat.S do Linux 4.12 :

 /* * 32-bit legacy system call entry. * * 32-bit x86 Linux system calls traditionally used the INT $0x80 * instruction. INT $0x80 lands here. * * This entry point can be used by 32-bit and 64-bit programs to perform * 32-bit system calls. Instances of INT $0x80 can be found inline in * various programs and libraries. It is also used by the vDSO's * __kernel_vsyscall fallback for hardware that doesn't support a faster * entry method. Restarted 32-bit system calls also fall back to INT * $0x80 regardless of what instruction was originally used to do the * system call. * * This is considered a slow path. It is not used by most libc * implementations on modern hardware except during process startup. ... */ ENTRY(entry_INT80_compat) ... (see the github URL for the full source) 

O código zero-extends eax em rax, então empurra todos os registradores para a pilha do kernel para formar um struct pt_regs . É onde ele será restaurado quando a chamada do sistema retornar. Ele está em um layout padrão para registros de espaço do usuário salvos (para qualquer ponto de input), assim ptrace de outro processo (como gdb ou strace ) lerá e / ou gravará essa memory se usar ptrace enquanto esse processo estiver dentro de uma chamada de sistema. (A modificação ptrace de registradores é uma coisa que torna os caminhos de retorno complicados para os outros pontos de input. Veja os comentários.)

Mas ele empurra $0 vez de r8 / r9 / r10 / r11. ( sysenter pontos de input sysenter e AMD syscall32 armazenam zeros para r8-r15.)

Eu acho que esse zeramento de r8-r11 é igualar o comportamento histórico. Antes de configurar pt_regs completos para todos os compat syscalls commit, o ponto de input apenas salvava os registradores c-clobbered. Ele foi enviado diretamente do asm com a call *ia32_sys_call_table(, %rax, 8) , e essas funções seguem a convenção de chamada, de modo que preservam o rbx , rbp , rsp e r12-r15 . Zerar o r8-r11 vez de deixá-los indefinidos foi provavelmente uma forma de evitar vazamentos de informação do kernel. IDK como ele manipulava o ptrace se a única cópia dos registros preservados de chamada do espaço do usuário estava na pilha do kernel onde uma function C os salvava. Duvido que tenha usado metadados de desenrolamento de pilha para encontrá-los lá.

A implementação atual (Linux 4.12) despacha chamadas de sistema ABI de 32 bits de C, recarregando o ebx , ecx , etc. pt_regs de pt_regs . (64-bit chamadas do sistema nativo despachar diretamente do asm, com apenas um mov %r10, %rcx necessário para explicar a pequena diferença na convenção de chamada entre funções e syscall . Infelizmente, nem sempre pode usar sysret , porque erros de CPU torná-lo não é seguro com endereços não canônicos. Ele tenta, então o caminho rápido é muito rápido, embora o syscall si ainda syscall dezenas de ciclos.)

De qualquer forma, no Linux atual, o syscalls de 32 bits (incluindo o int 0x80 de 64 bits) acabam sendo do_syscall_32_irqs_on(struct pt_regs *regs) . Ele despacha para um ponteiro de function ia32_sys_call_table , com 6 args estendido de zero. Isso talvez evite a necessidade de um wrapper em torno da function syscall nativa de 64 bits em mais casos para preservar esse comportamento, portanto, mais inputs da tabela ia32 podem ser a implementação da chamada do sistema nativo diretamente.

Linux 4.12 arch/x86/entry/common.c

 if (likely(nr < IA32_NR_syscalls)) { /* * It's possible that a 32-bit syscall implementation * takes a 64-bit parameter but nonetheless assumes that * the high bits are zero. Make sure we zero-extend all * of the args. */ 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); 

Em versões mais antigas do Linux que enviam chamadas de sistema de 32 bits do asm (como 64 bits ainda), o próprio ponto de input int80 coloca args nos registros corretos com instruções mov e xchg , usando registradores de 32 bits. Ele ainda usa mov %edx,%edx para estender o EDX em RDX (porque o arg3 usa o mesmo registrador em ambas as convenções). código aqui . Este código é duplicado nos pontos de input syscall32 e syscall32 .


Exemplo simples / programa de teste:

Eu escrevi um simples Hello World (na syntax NASM) que define todos os registradores para ter metades superiores diferentes de zero, então faz duas chamadas de sistema write() com int 0x80 , uma com um ponteiro para uma string em .rodata (sucede), o segundo com um ponteiro para a pilha (falha com -EFAULT ).

Em seguida, ele usa a ABI syscall nativa de 64 bits para write() os caracteres da pilha (ponteiro de 64 bits) e novamente para sair.

Portanto, todos esses exemplos estão usando as ABIs corretamente, exceto pelo segundo int 0x80 que tenta passar um ponteiro de 64 bits e truncá-lo.

Se você o construiu como um executável independente de posição, o primeiro também falhará. (Você teria que usar um lea relativo RIP em vez de mov para obter o endereço de hello: em um registro.)

Eu usei o gdb, mas use o depurador que você preferir. Use um que destaque os registros alterados desde a última etapa única. gdbgui funciona bem para depurar o código-fonte ASM, mas não é bom para a desassembly. Ainda assim, ele tem um painel de registro que funciona bem para regs de inteiro, pelo menos, e funcionou muito bem neste exemplo.

Veja o inline ;;; Comentários descrevendo como o registro é alterado por chamadas do sistema

 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 ;std after_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='bye\n' 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 = 14 section .rodata _start.hello: db "Hello World!", 0xa, 0 _start.hellolen equ $ - _start.hello 

Construa-o em um binário estático de 64 bits com

 yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm ld -o abi32-from-64 abi32-from-64.o 

Execute o gdb ./abi32-from-64 . No gdb , execute o ~/.gdbinit set disassembly-flavor intel e o layout reg se você não tiver isso em seu ~/.gdbinit . (GAS .intel_syntax é como MASM, não NASM, mas eles são próximos o suficiente para facilitar a leitura se você gosta da syntax NASM.)

 (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 

Pressione control-L quando o modo TUI do gdb ficar confuso. Isso acontece facilmente, mesmo quando os programas não são impressos para serem calculados.

Intereting Posts