Pergunta

Estou apenas começando a envolver minha cabeça de ponteiros de função em C. Para entender como o elenco de ponteiros de função funciona, escrevi o seguinte programa. Basicamente, cria um ponteiro de função para uma função que pega um parâmetro, o lança para um ponteiro de função com três parâmetros e chama a função, fornecendo três parâmetros. Eu estava curioso para saber o que aconteceria:

#include <stdio.h>

int square(int val){
  return val*val;
}

void printit(void* ptr){
  int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
  printf("Call function with parameters 2,4,8.\n");
  printf("Result: %d\n", fptr(2,4,8));
}


int main(void)
{
    printit(square);
    return 0;
}

Isso compila e funciona sem erros ou avisos (GCC -Wall no Linux / X86). A saída no meu sistema é:

Call function with parameters 2,4,8.
Result: 4

Então, aparentemente, os argumentos supérfluos são simplesmente descartados silenciosamente.

Agora eu gostaria de entender o que realmente está acontecendo aqui.

  1. Quanto à legalidade: se eu entendo a resposta para Lançar um ponteiro de função para outro tipo Corretamente, isso é simplesmente um comportamento indefinido. Portanto, o fato de isso correr e produzir um resultado razoável é apenas pura sorte, correto? (ou gentileza por parte dos escritores do compilador)
  2. Por que o GCC não me avisa sobre isso, mesmo com Wall? É algo que o compilador simplesmente não pode detectar? Por quê?

Estou vindo de Java, onde o TypeCecking é muito mais rigoroso, então esse comportamento me confundiu um pouco. Talvez eu esteja experimentando um choque cultural :-).

Foi útil?

Solução

Os parâmetros extras não são descartados. Eles são colocados corretamente na pilha, como se a chamada fosse feita para uma função que espera três parâmetros. No entanto, como sua função se importa com apenas um parâmetro, ela analisa apenas a parte superior da pilha e não toca nos outros parâmetros.

O fato de essa ligação funcionar é pura sorte, com base nos dois fatos:

  • O tipo do primeiro parâmetro é o mesmo para a função e o ponteiro fundido. Se você alterar a função para pegar um ponteiro para string e tentar imprimir essa string, você obterá uma boa falha, pois o código tentará desreferenciar o ponteiro para abordar a memória 2.
  • A convenção de chamada usada por padrão é que o chamador limpe a pilha. Se você alterar a convenção de chamada, para que o Callee limpe a pilha, você acabará com o chamador pressionando três parâmetros na pilha e, em seguida, a callee limpa (ou melhor, tentando) um parâmetro. Isso provavelmente levaria à corrupção da pilha.

Não há como o compilador avisá -lo sobre possíveis problemas como esse por uma simples razão - no caso geral, ele não conhece o valor do ponteiro no momento da compilação, para que não possa avaliar o que aponta. Imagine que o ponteiro da função aponta para um método em uma tabela virtual de classe criada em tempo de execução? Então, você diz ao compilador que é um ponteiro para uma função com três parâmetros, o compilador acreditará em você.

Outras dicas

Se você pegar um carro e lançá -lo como martelo, o compilador o levará à sua palavra de que o carro é um martelo, mas isso não transforma o carro em um martelo. O compilador pode ser bem -sucedido no uso do carro para acionar uma prego, mas isso é a boa sorte dependente da implementação. Ainda é uma coisa imprudente a se fazer.

  1. Sim, é um comportamento indefinido - tudo pode acontecer, incluindo que parece "funcionar".

  2. O elenco impede que o compilador emite um aviso. Além disso, os compiladores não têm necessidade de diagnosticar possíveis causas comportamentos indefinidos. A razão para isso é que é impossível fazê -lo, ou que isso seria muito difícil e/ou causaria muita sobrecarga.

A pior ofensa do seu elenco é lançar um ponteiro de dados para um ponteiro de função. É pior que a alteração da assinatura, porque não há garantia de que os tamanhos dos ponteiros da função e do ponteiro de dados sejam iguais. E ao contrário de muitos teórico Comportamentos indefinidos, este pode ser encontrado na natureza, mesmo em máquinas avançadas (não apenas em sistemas incorporados).

Você pode encontrar dicas de tamanho diferentes facilmente em plataformas incorporadas. Existem até processadores em que os ponteiros de dados e o ponteiro da função abordam coisas diferentes (RAM para uma, ROM para a outra), a chamada arquitetura de Harvard. No x86 no modo real, você pode ter 16 bits e 32 bits misturados. O Watcom-C tinha um modo especial para o DOS Extender, onde os ponteiros de dados tinham 48 bits de largura. Especialmente com C, deve -se saber que nem tudo é POSIX, pois C pode ser o único idioma disponível em hardware exótico.

Alguns compiladores permitem modelos de memória misturados, onde o código é garantido dentro de 32 bits tamanho e os dados são endereçados com ponteiros de 64 bits ou o converse.

Editar: CONCLUSÃO, NUNCA FOLTA UM POINTER DE DADOS PARA UM POINTER DE FUNÇÃO.

O comportamento é definido pela convenção de chamada. Se você usar uma convenção de chamada em que o chamador empurra e coloca a pilha, funcionaria bem neste caso, pois isso significaria que existem alguns bytes extras na pilha durante a chamada. Não tenho o GCC à mão no momento, mas com o compilador da Microsoft, este código:

int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);

A seguinte montagem é gerada para a chamada:

push        8
push        4
push        2
call        dword ptr [ebp-4]
add         esp,0Ch

Observe os 12 bytes (0ch) adicionados à pilha após a chamada. Depois disso, a pilha está bem (assumindo que o callee seja __cdecl neste caso, para que não tenta limpar também a pilha). Mas com o seguinte código:

int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);

o add esp,0Ch não é gerado na montagem. Se o callee for __cdecl neste caso, a pilha será corrompida.

  1. Cleante, não sei ao certo, mas você definitivamente não quer tirar proveito do comportamento se for a sorte ou Se for específico do compilador.

  2. Não merece um aviso porque o elenco é explícito. Ao elenco, você está informando ao compilador que conhece melhor. Em particular, você está lançando um void*, E, como tal, você está dizendo "Pegue o endereço representado por este ponteiro e faça o mesmo que este outro ponteiro" - o elenco simplesmente informa ao compilador que você tem certeza do que está no endereço de destino, de fato, de fato, o mesmo. Embora aqui, sabemos que isso está incorreto.

Devo refrescar minha memória do layout binário da Convenção de Calling C em algum momento, mas tenho certeza de que é isso que está acontecendo:

  • 1: Não é pura sorte. A convenção de chamada C é bem definida e dados extras na pilha não são um fator para o site de chamadas, embora possa ser substituído pelo Callee, pois o Callee não sabe disso.
  • 2: Um elenco "duro", usando parênteses, está dizendo ao compilador que você sabe o que está fazendo. Como todos os dados necessários estão em uma unidade de compilação, o compilador pode ser inteligente o suficiente para descobrir que isso é claramente ilegal, mas os designers da C não se concentraram em capturar incorreção verificável da capa. Simplificando, o compilador confia que você sabe o que está fazendo (talvez imprudentemente no caso de muitos programadores C/C ++!)

Para responder suas perguntas:

  1. Pure Luck - você pode facilmente pisar na pilha e substituir o ponteiro de retorno para o próximo código de execução. Como você especificou o ponteiro da função com 3 parâmetros e invocou o ponteiro da função, os dois parâmetros restantes foram "descartados" e, portanto, o comportamento é indefinido. Imagine se o 2º ou 3º parâmetro contivesse uma instrução binária, e tocasse a pilha de procedimentos de chamada ....

  2. Não há aviso que você estava usando um void * ponteiro e lançando -o. Esse é um código bastante legítimo aos olhos do compilador, mesmo que você tenha especificado explicitamente -Wall trocar. O compilador assume que você sabe o que está fazendo! Esse é o segredo.

Espero que isso ajude, cumprimentos, Tom.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top