Quais são as convenções de chamada para chamadas de sistema UNIX e Linux em i386 e x86-64
Pergunta
Os links a seguir explicam as convenções de chamada do sistema x86-32 para UNIX (versão BSD) e Linux:
Mas quais são as convenções de chamada do sistema x86-64 no UNIX e no Linux?
Solução
Leitura adicional para qualquer um dos tópicos aqui: O guia definitivo para chamadas de sistema Linux
Eu verifiquei isso usando GNU Assembler (gas) no Linux.
Interface do Kernel
Convenção de chamada do sistema Linux x86-32, também conhecida como i386:
Em x86-32, os parâmetros para chamadas de sistema Linux são passados usando registros. %eax
para syscall_number.%ebx, %ecx, %edx, %esi, %edi, %ebp são usados para passar 6 parâmetros para chamadas do sistema.
O valor de retorno está em %eax
.Todos os outros registros (incluindo EFLAGS) são preservados em todo o int $0x80
.
Peguei o seguinte trecho do Tutorial de montagem do Linux mas estou em dúvida sobre isso.Se alguém puder mostrar um exemplo, seria ótimo.
Se houver mais de seis argumentos,
%ebx
Deve conter o local da memória em que a lista de argumentos é armazenada - mas não se preocupe com isso, porque é improvável que você use um syscall com mais de seis argumentos.
Para um exemplo e um pouco mais de leitura, consulte http://www.int80h.org/bsdasm/#alternate-calling-convention.Outro exemplo de Hello World para i386 Linux usando int 0x80
: Quais partes deste código assembly HelloWorld são essenciais se eu escrever o programa em assembly?
Existe uma maneira mais rápida de fazer chamadas de sistema de 32 bits:usando sysenter
.O kernel mapeia uma página de memória em cada processo (o vDSO), com o lado do espaço do usuário do sysenter
dance, que tem que cooperar com o kernel para poder encontrar o endereço de retorno.Arg para registrar o mapeamento é o mesmo que para int $0x80
.Normalmente você deve ligar para o vDSO em vez de usar sysenter
diretamente.(Ver O guia definitivo para chamadas de sistema Linux para obter informações sobre como vincular e chamar o vDSO e para obter mais informações sobre sysenter
, e tudo o mais relacionado às chamadas do sistema.)
x86-32 [Free|Open|Net|DragonFly]Convenção de chamada do sistema BSD UNIX:
Os parâmetros são passados na pilha.Empurre os parâmetros (último parâmetro enviado primeiro) para a pilha.Em seguida, envie mais 32 bits de dados fictícios (na verdade, não são dados fictícios.consulte o link a seguir para obter mais informações) e, em seguida, forneça uma instrução de chamada do sistema int $0x80
http://www.int80h.org/bsdasm/#default-calling-convention
Convenção de chamada do sistema Linux x86-64:
x86-64 Mac OS X é semelhante, mas diferente.PENDÊNCIA:verifique o que o *BSD faz.
Consulte a seção:"A.2 AMD64 Linux Convenções do Kernel" de Suplemento de processador de arquitetura AMD64 de interface binária de aplicativo System V.As versões mais recentes dos psABIs i386 e x86-64 System V podem ser encontradas vinculado a esta página no repositório do mantenedor da ABI.(Veja também o x86 tag wiki para links ABI atualizados e muitas outras coisas boas sobre x86 asm.)
Aqui está o trecho desta seção:
- Os aplicativos no nível do usuário usam como registros inteiros para passar na sequência %rdi, %rsi, %rdx, %rcx, %r8 e %r9. A interface do kernel usa %rdi, %rsi, %rdx, %r10, %r8 e %r9.
- Uma chamada de sistema é feita através do
syscall
instrução.Esse derrota %rcx e %r11 bem como o valor de retorno% rax, mas outros registros são preservados.- O número do syscall deve ser passado no registrador %rax.
- As chamadas do sistema são limitadas a seis argumentos, nenhum argumento é passado diretamente na pilha.
- Retornando do syscall, o Register %Rax contém o resultado da chamada do sistema.Um valor no intervalo entre -4095 e -1 indica um erro, é
-errno
.- Somente valores da classe INTEGER ou da classe MEMORY são passados para o kernel.
Lembre-se de que isso consta do apêndice específico do Linux da ABI e, mesmo para Linux, é informativo e não normativo.(Mas é de fato preciso.)
Este 32 bits int $0x80
ABI é utilizável em código de 64 bits (mas altamente não recomendado). O que acontece se você usar o int 0x80 Linux ABI de 32 bits em código de 64 bits? Ele ainda trunca suas entradas para 32 bits, por isso é inadequado para ponteiros e zera r8-r11.
Interface de usuário:chamada de função
Convenção de chamada de função x86-32:
Em x86-32 os parâmetros foram passados na pilha.O último parâmetro foi colocado primeiro na pilha até que todos os parâmetros sejam concluídos e então call
instrução foi executada.Isso é usado para chamar funções da biblioteca C (libc) no Linux a partir do assembly.
Versões modernas do i386 System V ABI (usado no Linux) requerem alinhamento de 16 bytes de %esp
antes de call
, como o x86-64 System V ABI sempre exigiu.Os chamadores podem assumir isso e usar cargas/armazenamentos SSE de 16 bytes que falham no desalinhamento.Mas, historicamente, o Linux exigia apenas alinhamento de pilha de 4 bytes, por isso era necessário um trabalho extra para reservar espaço alinhado naturalmente, mesmo para uma pilha de 8 bytes. double
ou alguma coisa.
Alguns outros sistemas modernos de 32 bits ainda não exigem alinhamento de pilha de mais de 4 bytes.
Convenção de chamada de função do espaço do usuário x86-64 System V:
O x86-64 System V passa argumentos em registros, o que é mais eficiente do que a convenção de argumentos de pilha do i386 System V.Isso evita a latência e instruções extras de armazenar argumentos na memória (cache) e depois carregá-los novamente no receptor.Isso funciona bem porque há mais registros disponíveis e é melhor para CPUs modernas de alto desempenho, onde a latência e a execução fora de ordem são importantes.(A ABI i386 é muito antiga).
Nisso novo mecanismo:Primeiro os parâmetros são divididos em classes.A classe de cada parâmetro determina a maneira como ele é passado para a função chamada.
Para informações completas consulte:"3.2 Sequência de Chamada de Função" de Suplemento de processador de arquitetura AMD64 de interface binária de aplicativo System V que diz, em parte:
Depois que os argumentos são classificados, os registros são atribuídos (em ordem da esquerda para a direita) para a passagem da seguinte maneira:
- Se a classe for MEMÓRIA, passe o argumento na pilha.
- Se a classe for inteira, o próximo registro disponível da sequência %rdi, %rsi, %rdx, %rcx, %r8 e %r9 é usado
Então %rdi, %rsi, %rdx, %rcx, %r8 and %r9
são os registros em ordem usado para passar inteiro/ponteiro (ou seja,classe INTEGER) para qualquer função libc do assembly.%rdi é usado para o primeiro parâmetro INTEGER.%rsi para o 2º,% rdx para o 3º e assim por diante.Então call
instruções devem ser dadas.A pilha (%rsp
) deve estar alinhado com 16B quando call
executa.
Se houver mais de 6 parâmetros INTEGER, o 7º parâmetro INTEGER e posteriores serão passados na pilha.(O chamador aparece, igual ao x86-32.)
Os primeiros 8 argumentos de ponto flutuante são passados em% xmm0-7, posteriormente na pilha.Não há registros vetoriais preservados por chamadas.(Uma função com uma combinação de argumentos FP e inteiros pode ter mais de 8 argumentos de registro no total.)
Funções variáveis (como printf
) sempre preciso %al
= o número de argumentos do registro FP.
Existem regras para quando empacotar estruturas em registradores (rdx:rax
no retorno) vs.em memória.Consulte a ABI para obter detalhes e verifique a saída do compilador para garantir que seu código esteja de acordo com os compiladores sobre como algo deve ser passado/retornado.
Observe que a convenção de chamada de função do Windows x64 tem várias diferenças significativas em relação ao x86-64 System V, como o espaço de sombra que deve ser reservado pelo chamador (em vez de uma zona vermelha) e preservado por chamada xmm6-xmm15.E regras muito diferentes para quais argumentos entram em qual registro.
Outras dicas
Talvez você esteja procurando o X86_64 ABI?
- www.x86-64.org/documentation/abi.pdf (404 em 2018-11-24)
- www.x86-64.org/documentation/abi.pdf (Via Wayback Machine em 2018-11-24)
- Onde está documentado o sistema X86-64 V ABI? - https://github.com/hjl-tools/x86-psabi/wiki/x86-psabi é mantido atualizado (por HJ Lu, um dos mantenedores da ABI) com links para PDFs da versão atual oficial.
Se não for exatamente isso que você procura, use 'x86_64 ABI' no seu mecanismo de pesquisa preferido para encontrar referências alternativas.
As convenções de chamada definem como os parâmetros são passados nos registradores ao chamar ou serem chamados por outro programa.E a melhor fonte dessas convenções está na forma dos padrões ABI definidos para cada um desses hardwares.Para facilitar a compilação, a mesma ABI também é usada pelo espaço do usuário e pelo programa kernel.Linux/Freebsd segue a mesma ABI para x86-64 e outro conjunto para 32 bits.Mas o ABI x86-64 para Windows é diferente do Linux/FreeBSD.E geralmente a ABI não diferencia chamadas de sistema de "chamadas de funções" normais.Ou seja, aqui está um exemplo específico de convenções de chamada x86_64 e é o mesmo para o espaço de usuário e o kernel do Linux: http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/ (observe a sequência a,b,c,d,e,f de parâmetros):
O desempenho é uma das razões para essas ABI (por exemplo, passar parâmetros por meio de registros em vez de salvá-los em pilhas de memória).
Para ARM existem várias ABI:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.subset.swdev.abi/index.html
Convenção ARM64:
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
Para Linux em PowerPC:
http://refspecs.freestandards.org/elf/elfspec_ppc.pdf
http://www.0x04.net/doc/elf/psABI-ppc64.pdf
E para embarcados existe o PPC EABI:
http://www.freescale.com/files/32bit/doc/app_note/PPCEABI.pdf
Este documento é uma boa visão geral de todas as diferentes convenções:
Comentários de origem do kernel Linux 5.0
Eu sabia que as especificações do x86 estão abaixo arch/x86
, e esse material do syscall vai abaixo arch/x86/entry
.Então, um rápido git grep rdi
nesse diretório me leva a arco/x86/entrada/entrada_64.S:
/*
* 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
*
* This is the only entry point used for 64-bit system calls. The
* hardware interface is reasonably well designed and the register to
* argument mapping Linux uses fits well with the registers that are
* available when SYSCALL is used.
*
* SYSCALL instructions can be found inlined in libc implementations as
* well as some other programs and libraries. There are also a handful
* of SYSCALL instructions in the vDSO used, for example, as a
* clock_gettimeofday fallback.
*
* 64-bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11,
* then loads new ss, cs, and rip from previously programmed MSRs.
* rflags gets masked by a value from another MSR (so CLD and CLAC
* are not needed). SYSCALL does not save anything on the stack
* and does not change rsp.
*
* Registers on entry:
* rax system call number
* rcx return address
* r11 saved rflags (note: r11 is callee-clobbered register in C ABI)
* rdi arg0
* rsi arg1
* rdx arg2
* r10 arg3 (needs to be moved to rcx to conform to C ABI)
* r8 arg4
* r9 arg5
* (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
*
* Only called from user space.
*
* When user can change pt_regs->foo always force IRET. That is because
* it deals with uncanonical addresses better. SYSRET has trouble
* with them due to bugs in both AMD and Intel CPUs.
*/
e para 32 bits em arco/x86/entrada/entrada_32.S:
/*
* 32-bit SYSENTER entry.
*
* 32-bit system calls through the vDSO's __kernel_vsyscall enter here
* if X86_FEATURE_SEP is available. This is the preferred system call
* entry on 32-bit systems.
*
* The SYSENTER instruction, in principle, should *only* occur in the
* vDSO. In practice, a small number of Android devices were shipped
* with a copy of Bionic that inlined a SYSENTER instruction. This
* never happened in any of Google's Bionic versions -- it only happened
* in a narrow range of Intel-provided versions.
*
* SYSENTER loads SS, ESP, CS, and EIP from previously programmed MSRs.
* IF and VM in RFLAGS are cleared (IOW: interrupts are off).
* SYSENTER does not save anything on the stack,
* and does not save old EIP (!!!), ESP, or EFLAGS.
*
* To avoid losing track of EFLAGS.VM (and thus potentially corrupting
* user and/or vm86 state), we explicitly disable the SYSENTER
* instruction in vm86 mode by reprogramming the MSRs.
*
* Arguments:
* eax system call number
* ebx arg1
* ecx arg2
* edx arg3
* esi arg4
* edi arg5
* ebp user stack
* 0(%ebp) arg6
*/
Implementação de chamada de sistema glibc 2.29 Linux x86_64
Agora vamos trapacear observando as principais implementações da libc e ver o que elas estão fazendo.
O que poderia ser melhor do que examinar a glibc que estou usando agora enquanto escrevo esta resposta?:-)
glibc 2.29 define syscalls x86_64 em sysdeps/unix/sysv/linux/x86_64/sysdep.h
e que contém algum código interessante, por exemplo:
/* The Linux/x86-64 kernel expects the system call parameters in
registers according to the following table:
syscall number rax
arg 1 rdi
arg 2 rsi
arg 3 rdx
arg 4 r10
arg 5 r8
arg 6 r9
The Linux kernel uses and destroys internally these registers:
return address from
syscall rcx
eflags from syscall r11
Normal function call, including calls to the system call stub
functions in the libc, get the first six parameters passed in
registers and the seventh parameter and later on the stack. The
register use is as follows:
system call number in the DO_CALL macro
arg 1 rdi
arg 2 rsi
arg 3 rdx
arg 4 rcx
arg 5 r8
arg 6 r9
We have to take care that the stack is aligned to 16 bytes. When
called the stack is not aligned since the return address has just
been pushed.
Syscalls of more than 6 arguments are not supported. */
e:
/* Registers clobbered by syscall. */
# define REGISTERS_CLOBBERED_BY_SYSCALL "cc", "r11", "cx"
#undef internal_syscall6
#define internal_syscall6(number, err, arg1, arg2, arg3, arg4, arg5, arg6) \
({ \
unsigned long int resultvar; \
TYPEFY (arg6, __arg6) = ARGIFY (arg6); \
TYPEFY (arg5, __arg5) = ARGIFY (arg5); \
TYPEFY (arg4, __arg4) = ARGIFY (arg4); \
TYPEFY (arg3, __arg3) = ARGIFY (arg3); \
TYPEFY (arg2, __arg2) = ARGIFY (arg2); \
TYPEFY (arg1, __arg1) = ARGIFY (arg1); \
register TYPEFY (arg6, _a6) asm ("r9") = __arg6; \
register TYPEFY (arg5, _a5) asm ("r8") = __arg5; \
register TYPEFY (arg4, _a4) asm ("r10") = __arg4; \
register TYPEFY (arg3, _a3) asm ("rdx") = __arg3; \
register TYPEFY (arg2, _a2) asm ("rsi") = __arg2; \
register TYPEFY (arg1, _a1) asm ("rdi") = __arg1; \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (number), "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4), \
"r" (_a5), "r" (_a6) \
: "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
(long int) resultvar; \
})
que considero bastante autoexplicativos.Observe como isso parece ter sido projetado para corresponder exatamente à convenção de chamada das funções regulares do System V AMD64 ABI: https://en.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions
Lembrete rápido dos golpes:
cc
significa registros de bandeira.Mas Comentários de Pedro Cordes que isso é desnecessário aqui.memory
significa que um ponteiro pode ser passado em assembly e usado para acessar a memória
Para obter um exemplo executável mínimo explícito do zero, veja esta resposta: Como invocar uma chamada de sistema via sysenter em assembly inline?
Faça alguns syscalls em assembly manualmente
Não é muito científico, mas divertido:
x86_64.S
.text .global _start _start: asm_main_after_prologue: /* write */ mov $1, %rax /* syscall number */ mov $1, %rdi /* stdout */ mov $msg, %rsi /* buffer */ mov $len, %rdx /* len */ syscall /* exit */ mov $60, %rax /* syscall number */ mov $0, %rdi /* exit status */ syscall msg: .ascii "hello\n" len = . - msg
aarch64
Mostrei um exemplo mínimo de ambiente de usuário executável em: https://reverseengineering.stackexchange.com/questions/16917/arm64-syscalls-table/18834#18834 O código do kernel TODO grep aqui deve ser fácil.