A AMD tem uma especificação ABI que descreve a convenção de chamada para usar em x86-64. Todos os sistemas operacionais o seguem, exceto o Windows, que tem sua própria convenção de chamada x86-64. Por quê?
Alguém conhece as razões técnicas, históricas ou políticas para essa diferença, ou é puramente uma questão de síndrome NIH?
Eu entendo que diferentes sistemas operacionais podem ter necessidades diferentes para coisas de nível superior, mas isso não explica por que, por exemplo, a ordem de passagem do parâmetro de registro no Windows é rcx - rdx - r8 - r9 - rest on stack
enquanto todos os outros usam rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
.
PS Estou ciente de como essas convenções de chamada diferem em geral e sei onde encontrar detalhes se precisar. O que eu quero saber é por quê .
Editar: para saber como, consulte, por exemplo, a entrada da wikipedia e os links daí.
fonte
Respostas:
Escolhendo quatro registradores de argumento em x64 - comum a UN * X / Win64
Uma das coisas a se ter em mente sobre o x86 é que o nome do registro para a codificação de "número de registro" não é óbvio; em termos de codificação de instrução (o byte MOD R / M , consulte http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ), os números de registro 0 ... 7 são - nessa ordem -
?AX
,?CX
,?DX
,?BX
,?SP
,?BP
,?SI
,?DI
.Portanto, escolher A / C / D (regs 0..2) para o valor de retorno e os dois primeiros argumentos (que é a
__fastcall
convenção "clássica" de 32 bits ) é uma escolha lógica. No que diz respeito a 64 bits, os regs "superiores" são solicitados e tanto a Microsoft quanto o UN * X / Linux optaram porR8
/R9
como os primeiros.Mantendo isso em mente, a escolha da Microsoft
RAX
(valor de retorno) eRCX
,RDX
,R8
,R9
(arg [0..3]) são uma selecção compreensível se você escolher quatro registros achados para argumentos.Não sei por que o AMD64 UN * X ABI escolheu
RDX
antesRCX
.Escolhendo seis registros de argumento em x64 - específico de UN * X
UN * X, em arquiteturas RISC, tradicionalmente tem feito passagem de argumentos em registros - especificamente, para os primeiros seis argumentos (isso é assim em PPC, SPARC, MIPS pelo menos). Essa pode ser uma das principais razões pelas quais os designers da ABI AMD64 (UN * X) optaram por usar seis registradores também nessa arquitetura.
Então se você quer seis registros para passar argumentos em, e é lógico escolher
RCX
,RDX
,R8
eR9
para quatro deles, que outros dois você deve escolher?Os regs "mais altos" requerem um byte de prefixo de instrução adicional para selecioná-los e, portanto, têm uma pegada de tamanho de instrução maior, então você não gostaria de escolher qualquer um deles se tiver opções. Dos registros clássicos, devido ao significado implícito de
RBP
eRSP
estes não estão disponíveis, eRBX
tradicionalmente tem um uso especial em UN * X (tabela de deslocamento global) com o qual aparentemente os designers do AMD64 ABI não queriam se tornar incompatíveis desnecessariamente.Portanto, a única escolha era
RSI
/RDI
.Portanto, se você tiver que tomar
RSI
/RDI
como registradores de argumento, quais argumentos eles devem ser?Fazê-los
arg[0]
earg[1]
tem algumas vantagens. Veja o comentário de cHao.?SI
e?DI
são operandos de origem / destino de instrução de string, e como cHao mencionado, seu uso como registradores de argumento significa que, com as convenções de chamada AMD64 UN * X, astrcpy()
função mais simples possível , por exemplo, consiste apenas nas duas instruções de CPUrepz movsb; ret
porque a origem / destino endereços foram colocados nos registros corretos pelo chamador. Existe, particularmente no código de "cola" gerado pelo compilador e de baixo nível (pense, por exemplo, alguns alocadores de heap C ++ preenchendo objetos em construção ou as páginas de heap de preenchimento zero do kernel emsbrk()
, ou cópia -write pagefaults) uma enorme quantidade de cópia / preenchimento de bloco, portanto, será útil para o código tão freqüentemente usado para salvar as duas ou três instruções da CPU que, de outra forma, carregariam tais argumentos de endereço de origem / destino nos registros "corretos".Então, de certa forma, UN * X e Win64 são apenas diferentes em que UN * X "prepends" dois argumentos adicionais, em propositadamente escolhidas
RSI
/RDI
registadoras, para a escolha natural de quatro argumentos emRCX
,RDX
,R8
eR9
.Além disso ...
Existem mais diferenças entre os ABIs UN * X e Windows x64 do que apenas o mapeamento de argumentos para registros específicos. Para obter uma visão geral do Win64, verifique:
http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Win64 e AMD64 UN * X também diferem notavelmente na forma como o stackspace é usado; no Win64, por exemplo, o chamador deve alocar o espaço de pilha para os argumentos da função, mesmo que os argumentos 0 ... 3 sejam passados nos registradores. No UN * X, por outro lado, uma função folha (ou seja, uma que não chama outras funções) nem mesmo é necessária para alocar espaço de pilha se não precisar de mais de 128 bytes (sim, você possui e pode usar uma certa quantidade de pilha sem alocá-la ... bem, a menos que você seja o código do kernel, uma fonte de bugs bacanas). Todas essas são escolhas de otimização particulares, a maior parte da justificativa para elas é explicada nas referências ABI completas para as quais a referência da Wikipédia do autor original aponta.
fonte
__fastcall
são 100% idênticos para o caso de não ter mais de dois argumentos maiores que 32 bits e retornar um valor não maior que 32 bits. Essa não é uma pequena classe de funções. Essa compatibilidade com versões anteriores não é possível entre os ABIs UN * X para i386 / amd64.memcpy
pode ser implementado dessa forma, nãostrcpy
.IDK por que o Windows fez o que eles fizeram. Veja o final desta resposta para um palpite. Eu estava curioso para saber como a convenção de chamadas SysV foi decidida, então pesquisei no arquivo da lista de discussão e- e encontrei algumas coisas legais.
É interessante ler alguns desses tópicos antigos na lista de discussão do AMD64, uma vez que os arquitetos da AMD estavam ativos nisso. Por exemplo, escolher os nomes dos registros foi uma das partes difíceis: a AMD considerou renomear os 8 registros originais r0-r7, ou chamar os novos registros de coisas semelhantes
UAX
.Além disso, o feedback dos desenvolvedores do kernel identificou coisas que tornaram o design original
syscall
eswapgs
inutilizável . Foi assim que a AMD atualizou a instrução para resolver isso antes de lançar qualquer chip real. Também é interessante que, no final de 2000, a suposição era que a Intel provavelmente não adotaria o AMD64.A convenção de chamada SysV (Linux), e a decisão sobre quantos registros devem ser preservados pelo callee versus salvos pelo chamador, foi feita inicialmente em novembro de 2000, por Jan Hubicka (um desenvolvedor gcc). Ele compilou o SPEC2000 e examinou o tamanho do código e o número de instruções. Esse tópico de discussão pula em torno de algumas das mesmas idéias como respostas e comentários sobre esta pergunta SO. Em um segundo thread, ele propôs a sequência atual como ótima e, com sorte, final, gerando um código menor do que algumas alternativas .
Ele está usando o termo "global" para significar registros preservados de chamada, que devem ser push / popped se usados.
A escolha de
rdi
,rsi
,rdx
como os três primeiros argumentos foi motivada por:memset
ou outra função de string C em seus argumentos (onde gcc alinha uma operação de string rep?)rbx
é preservado por chamada porque ter dois regs preservados por chamada acessíveis sem prefixos REX (rbx e rbp) é uma vitória. Presumivelmente escolhido porque é o único outro reg que não é implicitamente usado por nenhuma instrução. (string de repetição, contagem de deslocamento e saídas / entradas mul / div afetam todo o resto).(background:
syscall
/sysret
inevitavelmente destróircx
(comrip
) er11
(comRFLAGS
), para que o kernel não possa ver o que estava originalmentercx
quandosyscall
executado.)A ABI de chamada de sistema do kernel foi escolhida para corresponder à ABI de chamada de função, exceto em
r10
vez dercx
, portanto, um wrapper libc funciona comommap(2)
pode apenasmov %rcx, %r10
/mov $0x9, %eax
/syscall
.Observe que a convenção de chamada SysV usada pelo Linux i386 é uma droga em comparação com o __vectorcall de 32 bits do Windows. Ele passa tudo na pilha e só retorna
edx:eax
para int64, não para pequenas estruturas . Não é nenhuma surpresa que pouco esforço foi feito para manter a compatibilidade com ele. Quando não há razão para não o fazer, eles fazem coisas como manterrbx
chamada preservada, já que decidiram que ter outro no 8 original (que não precisava de um prefixo REX) era bom.Tornar o ABI ideal é muito mais importante a longo prazo do que qualquer outra consideração. Eu acho que eles fizeram um bom trabalho. Não estou totalmente certo sobre como retornar structs compactados em registradores, em vez de campos diferentes em regs diferentes. Acho que o código que os passa por valor sem realmente operar nos campos vence dessa forma, mas o trabalho extra de desempacotar parece bobo. Eles poderiam ter mais registradores de retorno inteiros, mais do que apenas
rdx:rax
, então retornar uma estrutura com 4 membros poderia retorná-los em rdi, rsi, rdx, rax ou algo assim.Eles consideraram a passagem de inteiros em regs vetoriais, porque SSE2 pode operar em inteiros. Felizmente eles não fizeram isso. Os inteiros são usados como deslocamentos de ponteiro com muita freqüência, e uma viagem de ida e volta para a memória da pilha é muito barata . Além disso, as instruções SSE2 levam mais bytes de código do que as instruções de inteiros.
Eu suspeito que os designers de ABI do Windows podem ter buscado minimizar as diferenças entre 32 e 64 bits para o benefício de pessoas que precisam portar asm de um para o outro, ou que podem usar alguns
#ifdef
s em algum ASM para que a mesma fonte possa construir mais facilmente uma versão de 32 ou 64 bits de uma função.Minimizar as mudanças no conjunto de ferramentas parece improvável. Um compilador x86-64 precisa de uma tabela separada de qual registro é usado para quê e qual é a convenção de chamada. Ter uma pequena sobreposição com 32 bits provavelmente não produzirá economias significativas no tamanho / complexidade do código do conjunto de ferramentas.
fonte
Lembre-se de que a Microsoft foi inicialmente "oficialmente evasiva com o esforço inicial do AMD64" (de "A History of Modern 64-bit Computing" de Matthew Kerner e Neil Padgett) porque eram fortes parceiros da Intel na arquitetura IA64. Eu acho que isso significava que mesmo se eles estivessem abertos para trabalhar com os engenheiros do GCC em uma ABI para usar no Unix e no Windows, eles não teriam feito isso, pois significaria apoiar publicamente o esforço do AMD64 quando não o fizeram ainda não oficialmente feito (e provavelmente teria chateado a Intel).
Além disso, naquela época a Microsoft não tinha absolutamente nenhuma inclinação para ser amigável com projetos de código aberto. Certamente não Linux ou GCC.
Então, por que eles teriam cooperado em uma ABI? Eu acho que as ABIs são diferentes simplesmente porque foram projetadas mais ou menos ao mesmo tempo e de forma isolada.
Outra citação de "A History of Modern 64-bit Computing":
Isso indica que mesmo a AMD não sentiu que a cooperação era necessariamente a coisa mais importante entre MS e Unix, mas que ter suporte a Unix / Linux era muito importante. Talvez até tentar convencer um ou ambos os lados a se comprometer ou cooperar não valesse o esforço ou risco (?) De irritar qualquer um deles? Talvez a AMD tenha pensado que até mesmo sugerir uma ABI comum poderia atrasar ou inviabilizar o objetivo mais importante de simplesmente ter o suporte de software pronto quando o chip estivesse pronto.
Especulação da minha parte, mas acho que o principal motivo pelo qual as ABIs são diferentes foi a razão política de que o MS e o Unix / Linux não funcionaram juntos, e a AMD não viu isso como um problema.
fonte
__vectorcall
porque repassar__m128
a pilha era uma droga. Ter a semântica preservada de chamada para o 128b baixo de alguns dos regs vetoriais também é estranho (em parte, culpa da Intel por não projetar um mecanismo de salvamento / restauração extensível com SSE originalmente, e ainda não com AVX.)alloca
ou em alguns outros casos). Isso é normal se você está acostumado agcc -fomit-frame-pointer
ser o padrão no Linux. A ABI define metadados de desenrolamento de pilha que permitem que o tratamento de exceções ainda funcione. (Suponho que funcione algo como o material CFI do GNU / Linux x86-64 System V.eh_frame
).gcc -fomit-frame-pointer
tem sido o padrão (com otimização habilitada) desde sempre no x86-64, e outros compiladores (como o MSVC) fazem a mesma coisa.O Win32 tem seus próprios usos para ESI e EDI e requer que eles não sejam modificados (ou pelo menos que sejam restaurados antes de chamar a API). Eu imagino que o código de 64 bits faça o mesmo com RSI e RDI, o que explicaria por que eles não são usados para passar argumentos de função.
Eu não poderia dizer por que RCX e RDX foram trocados, no entanto.
fonte
__fastcall
convenção de chamadas. Você afirma que Win32 / Win64 não são compatíveis, mas então, olhe com atenção: para uma função que usa dois args de 32 bits e retorna 32 bits, Win64 e Win32__fastcall
na verdade são 100% compatíveis (os mesmos regs para passar dois argumentos de 32 bits, mesmo valor de retorno). Mesmo algum código binário (!) Pode funcionar em ambos os modos operacionais. O lado UNIX rompeu completamente com os "métodos antigos". Por boas razões, mas uma pausa é uma pausa.