Por que o Windows64 usa uma convenção de chamada diferente de todos os outros sistemas operacionais no x86-64?

A AMD possui uma especificação ABI que descreve a convenção de chamada a ser usada no x86-64. Todos os sistemas operacionais o seguem, exceto para o Windows que possui 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 de NIH?

Eu entendo que sistemas operacionais diferentes podem ter necessidades diferentes para coisas de nível mais alto, mas isso não explica porque, por exemplo, a ordem de passagem do parâmetro register no Windows é rcx - rdx - r8 - r9 - rest on stack enquanto todos 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 necessário. O que eu quero saber é porque .

Edit: para o como, veja, por exemplo, a input da wikipedia e links de lá.

Escolhendo quatro registradores de argumentos em x64 – comuns a UN * X / Win64

Uma das coisas que devemos ter em mente sobre o x86 é que o nome do registro para a codificação “reg number” não é óbvio; em termos de codificação de instruções (o byte MOD R / M , veja 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 convenção “clássica” de 32bit __fastcall ) é uma escolha lógica. No que diz respeito a 64 bits, os comandos “superiores” são ordenados, e tanto a Microsoft como o UN * X / Linux escolheram o R8 / R9 como os primeiros.

Tendo isso em mente, a escolha da Microsoft de RAX (valor de retorno) e RCX , RDX , R8 , R9 (arg [0..3]) é uma seleção compreensível se você escolher quatro registradores para argumentos.

Eu não sei porque o AMD64 UN * X ABI escolheu o RDX antes do RCX .

Escolhendo seis registradores de argumento em x64 – específico de UN * X

O UN * X, nas arquiteturas RISC, tradicionalmente faz o argumento passando nos registradores – especificamente, para os primeiros seis argumentos (isso é no PPC, SPARC, MIPS pelo menos). Qual pode ser uma das principais razões pelas quais os projetistas de ABI AMD64 (UN * X) escolheram usar também seis registros nessa arquitetura.

Então, se você quiser que seis registradores passem argumentos, e é lógico escolher o RCX , RDX , R8 e R9 para quatro deles, quais 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 um tamanho maior de tamanho de instrução, portanto, você não escolheria nenhum desses se tiver opções. Dos registros clássicos, devido ao significado implícito de RBP e RSP estes não estão disponíveis, e RBX tradicionalmente tem um uso especial em UN * X (tabela de offset global) que aparentemente os projetistas AMD64 ABI não querem desnecessariamente tornar-se incompatíveis com.
Ergo, a única escolha foi RSI / RDI .

Então, se você tem que tomar RSI / RDI como registradores de argumentos, quais argumentos eles deveriam ser?

Fazê-los arg[0] e arg[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 mencionado, seu uso como argumento registra que com as convenções de chamada AMD64 UN * X, a function strcpy() mais simples possível, por exemplo, consiste apenas dos dois Instruções da CPU repz movsb; ret repz movsb; ret porque os endereços de origem / destino foram colocados nos registros corretos pelo chamador. Há, particularmente em código de “cola” gerado por compilador e baixo nível (pense, por exemplo, em alocadores de heap C ++ com objects de preenchimento zero na construção, ou nas páginas de heap com preenchimento zero do kernel em sbrk() ou copy-on -write pagefaults) uma enorme quantidade de cópia / preenchimento de bloco, portanto, será útil para o código usado com tanta frequência para salvar as duas ou três instruções da CPU que carregariam esses argumentos de origem / destino nos registros “corretos”.

Então, de certa forma, UN * X e Win64 são diferentes apenas em que UN * X “prepends” dois argumentos adicionais, em propositadamente escolhidos registros RSI / RDI , para a escolha natural de quatro argumentos em RCX , RDX , R8 e R9 .

Além disso …

Existem mais diferenças entre as ABIs UN * X e Windows x64 do que apenas o mapeamento de argumentos para registros específicos. Para a visão geral sobre o Win64, verifique:

http://msdn.microsoft.com/pt-br/library/7kcdt6fy.aspx

O Win64 e o AMD64 UN * X também diferem notavelmente na maneira como o stackspace é usado; no Win64, por exemplo, o chamador deve alocar stackspace para argumentos de function, mesmo que args 0 … 3 sejam passados ​​em registradores. No UN * X, por outro lado, uma function folha (ou seja, uma que não chame outras funções) não precisa nem mesmo alocar espaço de pilha se não precisar mais do que 128 Bytes dela (sim, você possui e pode usar uma certa quantidade de pilha sem alocá-la … bem, a menos que você seja um código do kernel, uma fonte de bugs interessantes). Todas essas são opções de otimização específicas, a maior parte da justificativa para isso é explicada nas referências ABI completas às quais a referência da Wikipédia do pôster original aponta.

IDK porque o Windows fez o que eles fizeram. Veja o final desta resposta para um palpite. Eu estava curioso sobre como a convenção de chamada do SysV foi decidida, então eu procurei no arquivo da lista de discussão e encontrei algumas coisas legais.

É interessante ler alguns desses tópicos antigos na lista de discussão do AMD64, já que os arquitetos da AMD estavam ativos nele. Por exemplo, escolher nomes de registradores era uma das partes mais difíceis: a AMD considerou renomear os 8 registros originais r0-r7, ou chamar os novos registros de coisas como UAX .

Além disso, o feedback dos desenvolvedores do kernel identificou coisas que tornaram o design original do swapgs e do swapgs inutilizáveis . Foi assim que a AMD atualizou as instruções para resolver isso antes de liberar quaisquer chips reais. Também é interessante que, no final de 2000, a suposição era de que a Intel provavelmente não adotaria o AMD64.


A convenção de chamada do SysV (Linux) e a decisão sobre quantos registradores devem ser preservados de acordo com o call vs. save do chamador foram feitas inicialmente em Nov 2000, por Jan Hubicka (um desenvolvedor do 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 reflete algumas das mesmas idéias que respostas e comentários sobre essa questão. Em um segundo tópico, ele propôs a sequência atual como ótima e esperançosamente final, gerando código menor que algumas alternativas .

Ele está usando o termo “global” para significar registros preservados de chamadas, que precisam ser push / popped se usados.

A escolha de rdi , rsi , rdx como os três primeiros argumentos foi motivada por:

  • economia de tamanho de código menor em funções que chamam memset ou outra function de string C em seus args (onde o gcc ingressa uma operação de string rep?)
  • rbx é preservada por chamada porque ter dois rbx 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 turnos e saídas / inputs mul / div tocam em todo o resto).
  • Nenhum dos registradores com finalidades especiais é preservado por chamada (veja ponto anterior), então uma function que queira usar instruções de cadeia de caracteres rep ou um deslocamento de contagem variável pode ter que mover a function args para outro lugar, mas não precisa salvar / restaure o valor do chamador.
  • Estamos tentando evitar o RCX no início da sequência, já que é um registrador usado comumente para fins especiais, como o EAX, portanto, tem o mesmo propósito de estar ausente na sequência. Também não pode ser usado para syscalls e gostaríamos de fazer a sequência syscall para combinar a sequência de chamadas de function tanto quanto possível.

    (background: sysret / sysret inevitavelmente destruir rcx (com rip ) e RFLAGS (com RFLAGS ), então o kernel não pode ver o que estava originalmente no rcx quando o rcx foi executado.)

O ABI de chamada de sistema do kernel foi escolhido para corresponder à chamada de function ABI, exceto para r10 vez de rcx , portanto um wrapper libc funciona como mmap(2) pode apenas mov %rcx, %r10 / mov $0x9, %eax / syscall .


Observe que a convenção de chamada do SysV usada pelo i386 Linux é uma porcaria em comparação com a __vectorcall de 32 bits do Windows. Ele passa tudo na pilha e só retorna em edx:eax para int64, não para pequenas estruturas . Não é de surpreender que pouco esforço tenha sido feito para manter a compatibilidade com ele. Quando não há razão para não fazer isso, eles fizeram coisas como manter o rbx preservado, já que decidiram que ter outro no 8 original (que não precisa de um prefixo REX) era bom.

Tornar a ABI ótima é muito mais importante a longo prazo do que qualquer outra consideração. Eu acho que eles fizeram um ótimo trabalho. Eu não tenho certeza sobre o retorno de estruturas compactadas em registradores, em vez de diferentes campos em diferentes regs. Eu acho que o código que os transmite por valor sem realmente operar nos campos ganha dessa maneira, mas o trabalho extra de descompactar parece bobo. Eles poderiam ter registros de retorno mais inteiros, mais do que apenas rdx:rax , então retornar uma struct com 4 membros poderia retorná-los em rdi, rsi, rdx, rax ou algo assim.

Eles consideraram a passagem de números inteiros em vetores regs, porque o SSE2 pode operar em inteiros. Felizmente eles não fizeram isso. Inteiros são usados ​​como deslocamentos de ponteiro com muita frequência, e um round-trip para empilhar a memory é bem barato . Também as instruções SSE2 tomam mais bytes de código do que instruções inteiras.


Eu suspeito que os projetistas do Windows ABI poderiam ter procurado 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 crie facilmente uma versão de 32 ou 64 bits de uma function.

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 o que e qual é a convenção de chamada. É improvável que uma pequena sobreposição com 32 bits produza economias significativas no tamanho / complexidade do código do conjunto de ferramentas.

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 serem chamados na API). Eu imagino que o código de 64 bits faz o mesmo com o RSI e o RDI, o que explicaria por que eles não estão acostumados a passar argumentos de function.

Eu não posso te dizer porque o RCX e o RDX são trocados.

Lembre-se que a Microsoft foi inicialmente “oficialmente não comprometida com o esforço inicial da AMD64” (de “Uma História da Computação Moderna de 64 Bits” por Matthew Kerner e Neil Padgett) porque eles eram fortes parceiros da Intel na arquitetura IA64. Eu acho que isso significava que mesmo que eles estivessem abertos a trabalhar com engenheiros do GCC em uma ABI para usar tanto no Unix quanto no Windows, eles não o fariam, pois significaria apoiar publicamente o esforço do AMD64 quando não tivessem feito isso. ainda oficialmente feito (e provavelmente teria chateado a Intel).

Além disso, naqueles dias 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 diria que as ABIs são diferentes simplesmente porque foram projetadas mais ou menos ao mesmo tempo e isoladamente.

Outra citação de “A History of Modern 64-bit Computing”:

Em paralelo com a colaboração da Microsoft, a AMD também contratou a comunidade de código aberto para se preparar para o chip. A AMD contratou a Code Sorcery e a SuSE para o trabalho da cadeia de ferramentas (a Red Hat já estava envolvida pela Intel na porta da cadeia de ferramentas IA64). Russell explicou que o SuSE produzia compiladores C e FORTRAN, e o Code Sorcery produzia um compilador Pascal. Weber explicou que a empresa também se envolveu com a comunidade Linux para preparar uma porta Linux. Esse esforço foi muito importante: ele funcionou como um incentivo para a Microsoft continuar investindo no esforço do Windows AMD64, e também garantiu que o Linux, que estava se tornando um sistema operacional importante na época, estivesse disponível assim que os chips fossem lançados.

Weber chega a dizer que o trabalho com o Linux foi absolutamente crucial para o sucesso da AMD64, porque permitiu que a AMD produzisse um sistema de ponta a ponta sem a ajuda de outras empresas, se necessário. Essa possibilidade assegurou que a AMD tivesse uma estratégia de sobrevivência no pior caso, mesmo se outros parceiros desistissem, o que, por sua vez, mantinha os outros parceiros envolvidos com medo de ficarem para trás.

Isso indica que mesmo a AMD não sentiu que a cooperação era necessariamente a coisa mais importante entre o MS e o Unix, mas que ter suporte ao Unix / Linux era muito importante. Talvez até mesmo tentar convencer um ou ambos os lados a se comprometerem ou cooperar não valesse o esforço ou o risco (?) De irritar qualquer um deles? Talvez a AMD tenha pensado que até mesmo sugerir uma ABI comum poderia atrasar ou inviabilizar o objective mais importante de simplesmente ter suporte a software pronto quando o chip estivesse pronto.

Especulação de minha parte, mas acho que a principal razão pela qual as ABIs são diferentes foi a razão política pela qual os lados MS e Unix / Linux simplesmente não trabalharam juntos, e a AMD não viu isso como um problema.