stdcall e cdecl


89

Existem (entre outros) dois tipos de convenções de chamada - stdcall e cdecl . Eu tenho algumas perguntas sobre eles:

  1. Quando uma função cdecl é chamada, como um chamador sabe se ela deve liberar a pilha? No local da chamada, o chamador sabe se a função que está sendo chamada é cdecl ou uma função stdcall? Como funciona ? Como o chamador sabe se deve liberar a pilha ou não? Ou é a responsabilidade dos vinculadores?
  2. Se uma função declarada como stdcall chamar uma função (que tem uma convenção de chamada como cdecl), ou o contrário, isso seria inapropriado?
  3. Em geral, podemos dizer que chamada será mais rápida - cdecl ou stdcall?

9
Existem muitos tipos de convenções de chamada, das quais essas são apenas duas. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck

1
Por favor, marque uma resposta correta
ceztko

Respostas:


77

Raymond Chen oferece uma boa visão geral do que __stdcalle __cdeclfaz .

(1) O chamador "sabe" limpar a pilha depois de chamar uma função porque o compilador conhece a convenção de chamada dessa função e gera o código necessário.

void __stdcall StdcallFunc() {}

void __cdecl CdeclFunc()
{
    // The compiler knows that StdcallFunc() uses the __stdcall
    // convention at this point, so it generates the proper binary
    // for stack cleanup.
    StdcallFunc();
}

É possível divergir da convenção de chamada , assim:

LRESULT MyWndProc(HWND hwnd, UINT msg,
    WPARAM wParam, LPARAM lParam);
// ...
// Compiler usually complains but there's this cast here...
windowClass.lpfnWndProc = reinterpret_cast<WNDPROC>(&MyWndProc);

Tantas amostras de código erram, não é nem engraçado Deveria ser assim:

// CALLBACK is #define'd as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
    WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;

No entanto, presumindo que o programador não ignore os erros do compilador, o compilador gerará o código necessário para limpar a pilha de maneira adequada, pois conhecerá as convenções de chamada das funções envolvidas.

(2) Ambas as formas devem funcionar. Na verdade, isso acontece com bastante frequência, pelo menos no código que interage com a API do Windows, porque __cdeclé o padrão para programas C e C ++ de acordo com o compilador Visual C ++ e as funções WinAPI usam a __stdcallconvenção .

(3) Não deve haver nenhuma diferença real de desempenho entre os dois.


+1 para um bom exemplo e a postagem de Raymond Chen sobre a história da convenção de chamadas. Para qualquer pessoa interessada, as outras partes também são uma boa leitura.
OregonGhost

1 para Raymond Chen. Btw (OT): Por que não consigo encontrar as outras partes usando a caixa de pesquisa do blog? O Google os encontra, mas não os blogs do MSDN?
Nordic Mainframe

44

Em CDECL, os argumentos são colocados na pilha em ordem reversa, o chamador limpa a pilha e o resultado é retornado por meio do registro do processador (posteriormente chamarei de "registro A"). Em STDCALL há uma diferença, o chamador não limpa a pilha, mas a chamada faz.

Você está perguntando qual é mais rápido. Ninguém. Você deve usar a convenção de chamada nativa o máximo que puder. Altere a convenção apenas se não houver saída, ao usar bibliotecas externas que requerem o uso de certa convenção.

Além disso, existem outras convenções que o compilador pode escolher como padrão, isto é, o compilador Visual C ++ usa FASTCALL, que é teoricamente mais rápido devido ao uso mais extensivo de registros do processador.

Normalmente, você deve fornecer uma assinatura de convenção de chamada adequada para funções de retorno de chamada passadas para alguma biblioteca externa, ou seja, o retorno de chamada qsortda biblioteca C deve ser CDECL (se o compilador por padrão usa outra convenção, então devemos marcar o retorno de chamada como CDECL) ou vários retornos de chamada WinAPI devem ser STDCALL (WinAPI inteiro é STDCALL).

Outro caso comum pode ser quando você está armazenando ponteiros para algumas funções externas, por exemplo, para criar um ponteiro para a função WinAPI, sua definição de tipo deve ser marcada com STDCALL.

E abaixo está um exemplo que mostra como o compilador faz isso:

/* 1. calling function in C++ */
i = Function(x, y, z);

/* 2. function body in C++ */
int Function(int a, int b, int c) { return a + b + c; }

CDECL:

/* 1. calling CDECL 'Function' in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call (jump to function body, after function is finished it will jump back here, the address where to jump back is in registers)
move contents of register A to 'i' variable
pop all from the stack that we have pushed (copy of x, y and z)

/* 2. CDECL 'Function' body in pseudo-assembler */
/* Now copies of 'a', 'b' and 'c' variables are pushed onto the stack */
copy 'a' (from stack) to register A
copy 'b' (from stack) to register B
add A and B, store result in A
copy 'c' (from stack) to register B
add A and B, store result in A
jump back to caller code (a, b and c still on the stack, the result is in register A)

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */
pop 'a' from stack to register A
pop 'b' from stack to register B
add A and B, store result in A
pop 'c' from stack to register B
add A and B, store result in A
jump back to caller code (a, b and c are no more on the stack, result in register A)

Observação: __fastcall é mais rápido que __cdecl e STDCALL é a convenção de chamada padrão para Windows de 64 bits
dns

ohh; então ele deve exibir o endereço de retorno, adicionar o tamanho do bloco de argumento e, em seguida, pular para o endereço de retorno exibido anteriormente? (uma vez que você chamar ret, você não pode mais limpar a pilha, mas depois de limpar a pilha, você não pode mais chamar ret (uma vez que você precisaria se enterrar de volta para manter a pilha, trazendo-o de volta ao problema de que você não limpou a pilha.
Dmitry

alternativamente, retorne ao reg1, defina o ponteiro da pilha para o ponteiro da base e pule para reg1
Dmitry

alternativamente, mova o valor do ponteiro da pilha do topo para a base, limpe e chame ret
Dmitry

15

Notei uma postagem que diz que não importa se você chama a __stdcallde a __cdeclou vice-versa. É verdade.

O motivo: com __cdeclos argumentos que são passados ​​para as funções chamadas são removidos da pilha pela função de chamada, em __stdcall, os argumentos são removidos da pilha pela função chamada. Se você chamar uma __cdeclfunção com um __stdcall, a pilha não será limpa, então, eventualmente, quando o __cdeclusar uma referência baseada em pilha para argumentos ou endereço de retorno usará os dados antigos no ponteiro da pilha atual. Se você chamar uma __stdcallfunção de a __cdecl, a __stdcallfunção limpará os argumentos na pilha e, em seguida __cdecl, fará isso novamente, possivelmente removendo as informações de retorno das funções de chamada.

A convenção da Microsoft para C tenta contornar isso alterando os nomes. Uma __cdeclfunção é prefixada com um sublinhado. Uma __stdcallfunção é prefixada com um sublinhado e sufixada com uma arroba “@” e o número de bytes a serem removidos. Por exemplo, __cdeclf (x) está ligado como _f, __stdcall f(int x)está ligado como _f@4onde sizeof(int)está 4 bytes)

Se você conseguir passar do vinculador, aproveite a bagunça de depuração.


3

Eu quero melhorar a resposta de @ adf88. Eu sinto que o pseudocódigo para o STDCALL não reflete a forma como isso acontece na realidade. 'a', 'b' e 'c' não são retirados da pilha no corpo da função. Em vez disso, eles são retirados pela retinstrução ( ret 12seria usada neste caso) que em uma só investida volta para o chamador e ao mesmo tempo retira 'a', 'b' e 'c' da pilha.

Aqui está minha versão corrigida de acordo com o meu entendimento:

STDCALL:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then copy of 'y', then copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code and at the same time pop 'a', 'b' and 'c' off the stack (a, b and c are removed from the stack in this step, result in register A)


2

É especificado no tipo de função. Quando você tem um ponteiro de função, ele é considerado cdecl, se não explicitamente stdcall. Isso significa que se você obtiver um ponteiro stdcall e um ponteiro cdecl, não poderá trocá-los. Os dois tipos de função podem chamar um ao outro sem problemas, basta obter um tipo quando você espera o outro. Quanto à velocidade, os dois desempenham as mesmas funções, apenas em um lugar ligeiramente diferente, é realmente irrelevante.


1

O chamador e o receptor precisam usar a mesma convenção no ponto de invocação - essa é a única maneira de funcionar de forma confiável. Tanto o chamador quanto o receptor seguem um protocolo predefinido - por exemplo, quem precisa limpar a pilha. Se as convenções não corresponderem, seu programa terá um comportamento indefinido - provavelmente travará de maneira espetacular.

Isso só é necessário por site de invocação - o próprio código de chamada pode ser uma função com qualquer convenção de chamada.

Você não deve notar nenhuma diferença real no desempenho entre essas convenções. Se isso se tornar um problema, você geralmente precisará fazer menos chamadas - por exemplo, altere o algoritmo.


1

Essas coisas são específicas do compilador e da plataforma. Nem o padrão C nem o C ++ dizem nada sobre as convenções de chamada, exceto extern "C"em C ++.

como um chamador sabe se deve liberar a pilha?

O chamador conhece a convenção de chamada da função e trata a chamada de acordo.

No local da chamada, o chamador sabe se a função que está sendo chamada é cdecl ou uma função stdcall?

Sim.

Como funciona ?

Faz parte da declaração da função.

Como o chamador sabe se deve liberar a pilha ou não?

O chamador conhece as convenções de chamada e pode agir de acordo.

Ou é a responsabilidade dos vinculadores?

Não, a convenção de chamada é parte da declaração de uma função para que o compilador saiba tudo o que precisa saber.

Se uma função declarada como stdcall chamar uma função (que tem uma convenção de chamada como cdecl), ou o contrário, isso seria inapropriado?

Não. Por que deveria?

Em geral, podemos dizer que chamada será mais rápida - cdecl ou stdcall?

Eu não sei. Teste-o.


0

a) Quando uma função cdecl é chamada pelo chamador, como um chamador sabe se deve liberar a pilha?

O cdeclmodificador é parte do protótipo da função (ou tipo de ponteiro de função, etc.) para que o chamador obtenha as informações de lá e aja de acordo.

b) Se uma função declarada como stdcall chamar uma função (que tem uma convenção de chamada como cdecl), ou o contrário, isso seria inapropriado?

Não, está bem.

c) Em geral, podemos dizer que chamada será mais rápida - cdecl ou stdcall?

Em geral, eu me absteria de fazer tais declarações. A distinção importa, por exemplo. quando você deseja usar funções va_arg. Em teoria, pode ser que stdcallseja mais rápido e gere um código menor porque permite combinar o popping dos argumentos com o popping dos locais, mas com OTOH cdecl, você pode fazer a mesma coisa também, se for inteligente.

As convenções de chamada que visam ser mais rápidas geralmente fazem alguma passagem de registro.


-1

As convenções de chamada não têm nada a ver com as linguagens de programação C / C ++ e são bastante específicas sobre como um compilador implementa a linguagem fornecida. Se você usa consistentemente o mesmo compilador, nunca precisa se preocupar com convenções de chamada.

No entanto, às vezes queremos código binário compilado por diferentes compiladores para interoperar corretamente. Quando fazemos isso, precisamos definir algo chamado Application Binary Interface (ABI). A ABI define como o compilador converte a fonte C / C ++ em código de máquina. Isso incluirá convenções de chamada, alteração de nome e layout de tabela v. cdelc e stdcall são duas convenções de chamada diferentes comumente usadas em plataformas x86.

Colocando as informações sobre a convenção de chamada no cabeçalho de origem, o compilador saberá qual código precisa ser gerado para interoperar corretamente com o executável fornecido.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.