Por que ponteiros de função e ponteiros de dados são incompatíveis em C / C ++?

130

Eu li que converter um ponteiro de função em um ponteiro de dados e vice-versa funciona na maioria das plataformas, mas não é garantido que funcione. Por que esse é o caso? Os dois não deveriam ser simplesmente endereços na memória principal e, portanto, ser compatíveis?

gexicida
fonte
16
Indefinido no padrão C, definido no POSIX. Mente a diferença.
ephemient 07/02
Eu sou um pouco novo nisso, mas você não deveria fazer o elenco do lado direito do "="? Parece-me que o problema é que você está atribuindo um ponteiro nulo. Mas vejo que a página de manual faz isso, por isso espero que alguém possa me educar. Vejo exemplos na 'rede de pessoas que lançam o valor de retorno do dlsym, por exemplo, aqui: daniweb.com/forums/thread62561.html
JasonWoof
9
Observe o que o POSIX diz na seção Tipos de dados : §2.12.3 Tipos de ponteiros. Todos os tipos de ponteiros de função devem ter a mesma representação que o ponteiro de tipo para void. A conversão de um ponteiro de função para void *não deve alterar a representação. Um void *valor resultante dessa conversão pode ser convertido novamente no tipo de ponteiro da função original, usando uma conversão explícita, sem perda de informações. Nota : O padrão ISO C não exige isso, mas é necessário para conformidade com POSIX.
Jonathan Leffler
2
esta é a pergunta na seção SOBRE deste site .. :) :) Veja sua pergunta aqui
ZooZ
1
@ KeithThompson: o mundo muda - e o POSIX também. O que escrevi em 2012 não se aplica mais em 2018. O padrão POSIX mudou a verborragia. Agora está associado a dlsym()- observe o final da seção 'Uso de aplicativos', onde diz: Observe que a conversão de um void *ponteiro para um ponteiro de função como em: fptr = (int (*)(int))dlsym(handle, "my_function"); não é definida pelo padrão ISO C. Este padrão requer que esta conversão funcione corretamente em implementações em conformidade.
Jonathan Leffler 31/07

Respostas:

171

Uma arquitetura não precisa armazenar código e dados na mesma memória. Com uma arquitetura de Harvard, o código e os dados são armazenados em uma memória completamente diferente. A maioria das arquiteturas são arquiteturas de Von Neumann com código e dados na mesma memória, mas C não se limita a apenas certos tipos de arquiteturas, se possível.

Dirk Holsopple
fonte
15
Além disso, mesmo que o código e os dados sejam armazenados no mesmo local no hardware físico, o acesso ao software e à memória geralmente impede a execução de dados como código sem a "aprovação" do sistema operacional. DEP e similares.
Michael Graczyk
15
Pelo menos tão importante quanto ter diferentes espaços de endereço (talvez mais importante) é que os ponteiros de função podem ter uma representação diferente dos ponteiros de dados.
22712 Michael Burr
14
Você nem precisa ter uma arquitetura de Harvard para ter ponteiros de código e dados usando diferentes espaços de endereço - o antigo modelo de memória "Pequeno" do DOS fazia isso (perto de ponteiros com CS != DS).
caf
1
até os processadores modernos enfrentam essa mistura, já que o cache de instruções e dados geralmente é tratado separadamente, mesmo quando o sistema operacional permite que você escreva código em algum lugar.
PypeBros 11/09/12
3
@EricJ. Até você ligar VirtualProtect, o que permite marcar regiões de dados como executáveis.
Dietrich Epp
37

Alguns computadores possuem (tinham) espaços de endereço separados para código e dados. Em tal hardware, simplesmente não funciona.

O idioma foi projetado não apenas para aplicativos de desktop atuais, mas para permitir sua implementação em um grande conjunto de hardware.


Parece que o comitê de linguagem C nunca pretendeu void*ser um ponteiro para funcionar, eles apenas queriam um ponteiro genérico para objetos.

A justificativa do C99 diz:

6.3.2.3 Os ponteiros
C foram implementados em uma ampla variedade de arquiteturas. Enquanto algumas dessas arquiteturas apresentam ponteiros uniformes com o tamanho de algum tipo inteiro, o código portátil máximo não pode assumir nenhuma correspondência necessária entre os diferentes tipos de ponteiros e os tipos inteiros. Em algumas implementações, os ponteiros podem até ser mais largos do que qualquer tipo inteiro.

O uso de void*("ponteiro para void") como um tipo genérico de ponteiro de objeto é uma invenção do Comitê C89. A adoção desse tipo foi estimulada pelo desejo de especificar argumentos de protótipo de função que convertem silenciosamente ponteiros arbitrários (como em fread) ou reclamam se o tipo de argumento não corresponde exatamente (como em strcmp). Nada é dito sobre ponteiros para funções, que podem ser incomensuráveis ​​com ponteiros de objetos e / ou números inteiros.

Nota Nada é dito sobre ponteiros para funções no último parágrafo. Eles podem ser diferentes de outros indicadores, e o comitê está ciente disso.

Bo Persson
fonte
O padrão pode torná-los compatíveis sem interferir nisso, simplesmente tornando os tipos de dados do mesmo tamanho e garantindo que atribuir a um e depois retornar resultará no mesmo valor. Eles fazem isso com void *, que é o único tipo de ponteiro compatível com tudo.
Edward Strange
15
@CrazyEddie Você não pode atribuir um ponteiro de função a void *.
ouah
4
Eu poderia estar errado ao aceitar * ponteiros de função, mas o ponto permanece. Bits são bits. O padrão poderia exigir que o tamanho dos diferentes tipos pudesse acomodar os dados um do outro e a atribuição seria garantida para funcionar, mesmo se eles forem usados ​​em diferentes segmentos de memória. O motivo dessa incompatibilidade existir é que isso NÃO é garantido pelo padrão e, portanto, os dados podem ser perdidos na atribuição.
Edward Strange
5
Porém, exigir sizeof(void*) == sizeof( void(*)() )isso desperdiçaria espaço no caso de ponteiros de função e ponteiros de dados terem tamanhos diferentes. Este era um caso comum nos anos 80, quando o primeiro padrão C foi escrito.
Robᵩ
8
@RichardChambers: os diferentes espaços de endereço também podem ter diferentes larguras de endereço , como um Atmel AVR que usa 16 bits para instruções e 8 bits para dados; nesse caso, seria difícil converter dados de ponteiros de dados (8 bits) em funções (16 bits) e vice-versa. C deve ser fácil de implementar; parte dessa facilidade vem de deixar os ponteiros de dados e instruções incompatíveis entre si.
John Bode
30

Para aqueles que se lembram do MS-DOS, Windows 3.1 e anteriores, a resposta é bastante fácil. Tudo isso usado para suportar vários modelos de memória diferentes, com combinações variadas de características para indicadores de código e dados.

Por exemplo, para o modelo Compact (código pequeno, dados grandes):

sizeof(void *) > sizeof(void(*)())

e, inversamente, no modelo Médio (código grande, dados pequenos):

sizeof(void *) < sizeof(void(*)())

Nesse caso, você não tinha armazenamento separado para código e data, mas ainda não conseguiu converter entre os dois ponteiros (exceto por usar modificadores __near e __far não padronizados).

Além disso, não há garantia de que, mesmo que os ponteiros tenham o mesmo tamanho, eles apontem para a mesma coisa - no modelo de memória DOS pequeno, código e dados usados ​​perto de ponteiros, mas apontaram para segmentos diferentes. Portanto, converter um ponteiro de função em um ponteiro de dados não forneceria um ponteiro que tivesse qualquer relação com a função e, portanto, não havia utilidade para essa conversão.

Tomek
fonte
Re: "converter um ponteiro de função em um ponteiro de dados não daria a você um ponteiro que tivesse qualquer relação com a função e, portanto, não havia utilidade para essa conversão": isso não ocorre inteiramente. Converter um int*em um void*fornece um ponteiro com o qual você realmente não pode fazer nada, mas ainda é útil poder realizar a conversão. (Isso ocorre porque void*pode armazenar qualquer ponteiro de objeto, portanto, pode ser usado para algoritmos genéricos que não precisam saber qual o tipo que eles possuem. O mesmo poderia ser útil para ponteiros de função, se fosse permitido.)
ruakh
4
@ruakh: No caso de converter o int *para void *, void *é garantido que pelo menos aponte para o mesmo objeto que o original int *- portanto, isso é útil para algoritmos genéricos que acessam o objeto apontado, como int n; memcpy(&n, src, sizeof n);. No caso em que a conversão de um ponteiro de função para a void *não produz um ponteiro apontando para a função, não é útil para tais algoritmos - a única coisa que você pode fazer é converter a void *volta para um ponteiro de função novamente, portanto, você pode bem, basta usar um unioncontendo um void *ponteiro e função.
caf
@caf: justo o suficiente. Obrigado por apontar isso. E, nesse caso, mesmo void* que aponte para a função, suponho que seria uma má idéia para as pessoas passá-la memcpy. :-P
ruakh 11/09/12
Copiado de cima: Observe o que o POSIX diz em Tipos de dados : §2.12.3 Tipos de ponteiros. Todos os tipos de ponteiros de função devem ter a mesma representação que o ponteiro de tipo para void. A conversão de um ponteiro de função para void *não deve alterar a representação. Um void *valor resultante dessa conversão pode ser convertido novamente no tipo de ponteiro da função original, usando uma conversão explícita, sem perda de informações. Nota : O padrão ISO C não exige isso, mas é necessário para conformidade com POSIX.
Jonathan Leffler
@caf Se for apenas repassar para algum retorno de chamada que sabe o tipo adequado, estou interessado apenas na segurança de ida e volta, e não em qualquer outro relacionamento que esses valores convertidos possam ter.
Deduplicator
23

Os ponteiros para anular devem ser capazes de acomodar um ponteiro para qualquer tipo de dados - mas não necessariamente um ponteiro para uma função. Alguns sistemas têm requisitos diferentes para ponteiros para funções e ponteiros para dados (por exemplo, existem DSPs com endereços diferentes para dados versus código, o modelo médio no MS-DOS usou ponteiros de 32 bits para código, mas apenas ponteiros de 16 bits para dados) .

Jerry Coffin
fonte
1
mas a função dlsym () não deveria retornar algo diferente de um void *. Quero dizer, se o vazio * não é grande o suficiente para o ponteiro de função, já não estamos confusos?
Manav
1
@ Knickerkicker: Sim, provavelmente. Se a memória servir, o tipo de retorno do dlsym foi discutido detalhadamente, provavelmente 9 ou 10 anos atrás, na lista de e-mails do OpenGroup. De imediato, não me lembro o que (se alguma coisa) resultou disso.
Jerry Coffin
1
você está certo. Este parece um resumo bastante bom (embora desatualizado) do seu argumento.
Manav
2
@LegoStormtroopr: Interessante a maneira como 21 pessoas concordam com a idéia de votação antecipada, mas apenas cerca de 3 realmente o fizeram. :-)
Jerry Coffin
13

Além do que já foi dito aqui, é interessante observar o POSIX dlsym():

O padrão ISO C não exige que ponteiros para funções possam ser convertidos para ponteiros para dados. De fato, o padrão ISO C não exige que um objeto do tipo void * possa conter um ponteiro para uma função. As implementações que suportam a extensão XSI, no entanto, exigem que um objeto do tipo void * possa conter um ponteiro para uma função. O resultado da conversão de um ponteiro para uma função em um ponteiro para outro tipo de dados (exceto nulo *) ainda é indefinido, no entanto. Observe que os compiladores em conformidade com o padrão ISO C são necessários para gerar um aviso se uma conversão de um ponteiro void * em um ponteiro de função for tentada como em:

 fptr = (int (*)(int))dlsym(handle, "my_function");

Devido ao problema observado aqui, uma versão futura pode adicionar uma nova função para retornar ponteiros de função ou a interface atual pode ser preterida em favor de duas novas funções: uma que retorna ponteiros de dados e a outra que retorna ponteiros de função.

Maxim Egorushkin
fonte
isso significa que o uso do dlsym para obter o endereço de uma função não é seguro no momento? Existe atualmente uma maneira segura de fazer isso?
Gexicide
4
Isso significa que atualmente o POSIX exige de uma plataforma ABI que os indicadores de função e de dados possam ser convertidos com segurança void*.
Maxim Egorushkin
@gexicide Significa que as implementações que são compatíveis com POSIX fizeram uma extensão para a linguagem, dando um significado definido para a implementação do que é um comportamento indefinido para o próprio padrão. Ele é listado como uma das extensões comuns ao padrão C99, seção J.5.7.
David Hammen
1
@DavidHammen Não é uma extensão do idioma, mas um novo requisito extra. C não precisa void*ser compatível com um ponteiro de função, enquanto o POSIX exige .
Maxim Egorushkin
9

O C ++ 11 possui uma solução para a incompatibilidade de longa data entre C / C ++ e POSIX em relação a dlsym(). Pode-se usarreinterpret_cast para converter um ponteiro de função para / de um ponteiro de dados, desde que a implementação suporte esse recurso.

A partir do padrão, 5.2.10 para. 8, "a conversão de um ponteiro de função em um tipo de ponteiro de objeto ou vice-versa é suportada condicionalmente". 1.3.5 define "com suporte condicional" como uma "construção de programa que uma implementação não precisa suportar".

David Hammen
fonte
Pode-se, mas não se deve. Um compilador em conformidade deve gerar um aviso para isso (que por sua vez deve disparar um erro, cf. -Werror). Uma solução melhor (e não UB) é recuperar um ponteiro para o objeto retornado por dlsym(ie void**) e convertê-lo em um ponteiro para funcionar com o ponteiro . Ainda definido pela implementação, mas não causa mais um aviso / erro .
11289 Konrad Rudolph
3
@KonradRudolph: Discordo. O texto "suportado condicionalmente" foi escrito especificamente para permitir dlsyme GetProcAddresscompilar sem aviso.
MSalters
@MSalters O que você quer dizer com "discordar"? Ou estou certo ou errado. A documentação do dlsym diz explicitamente que “compiladores em conformidade com o padrão ISO C são necessários para gerar um aviso se for tentada uma conversão de um ponteiro * vazio para um ponteiro de função”. Isso não deixa muito espaço para especulações. E GCC (com -pedantic) que advertem. Novamente, nenhuma especulação é possível.
21978 Konrad Rudolph
1
Acompanhamento: acho que agora entendo. Não é UB. É definido pela implementação. Ainda não tenho certeza se o aviso deve ser gerado ou não - provavelmente não. Ah bem.
11289 Konrad Rudolph
2
@ KonradRudolph: Eu discordei do seu "não deveria", o que é uma opinião. A resposta mencionou especificamente o C ++ 11, e eu era membro do C ++ CWG no momento em que o problema foi solucionado. De fato, o C99 possui uma redação diferente; o suporte condicional é uma invenção do C ++.
MSalters
7

Dependendo da arquitetura de destino, o código e os dados podem ser armazenados em áreas da memória fundamentalmente incompatíveis e fisicamente distintas.

Graham Borland
fonte
"fisicamente distinto" eu entendo, mas você pode elaborar mais sobre a distinção "fundamentalmente incompatível". Como eu disse na pergunta, não é um ponteiro vazio do tamanho de qualquer tipo de ponteiro - ou é uma presunção errada da minha parte.
Manav
@ KnickerKicker: void *é grande o suficiente para armazenar qualquer ponteiro de dados, mas não necessariamente qualquer ponteiro de função.
ephemient
1
de volta para o futuro: P
SSpoke
5

undefined não significa necessariamente não permitido, pode significar que o implementador do compilador tem mais liberdade para fazê-lo como quiser.

Por exemplo, pode não ser possível em algumas arquiteturas - indefinido permite que eles ainda tenham uma biblioteca 'C' em conformidade, mesmo que você não possa fazer isso.

Martin Beckett
fonte
5

Outra solução:

Supondo que o POSIX garanta que os ponteiros de função e dados tenham o mesmo tamanho e representação (não consigo encontrar o texto para isso, mas o exemplo do OP citado sugere que eles pelo menos pretendiam fazer esse requisito), o seguinte deve funcionar:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Isso evita violar as regras de aliasing, passando pelo char [] representação, que é permitida para alias todos os tipos.

Ainda outra abordagem:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Mas eu recomendaria a memcpyabordagem se você quiser absolutamente 100% de C. correto

R .. GitHub PARE DE AJUDAR GELO
fonte
5

Eles podem ser diferentes tipos com diferentes requisitos de espaço. Atribuir a um pode fatiar irreversivelmente o valor do ponteiro, de modo que atribuir de volta resulta em algo diferente.

Eu acredito que eles podem ser de tipos diferentes porque o padrão não deseja limitar possíveis implementações que economizam espaço quando não é necessário ou quando o tamanho pode fazer com que a CPU precise fazer uma porcaria extra para usá-lo, etc.

Edward Strange
fonte
3

A única solução verdadeiramente portátil é não usar dlsympara funções e, em vez disso, usar dlsympara obter um ponteiro para dados que contém ponteiros de função. Por exemplo, na sua biblioteca:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

e depois no seu aplicativo:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

De qualquer forma, essa é uma boa prática de design e facilita o suporte ao carregamento dinâmico por meio de dlopenvinculação estática e todos os módulos em sistemas que não suportam a vinculação dinâmica ou onde o usuário / integrador de sistemas não deseja usar a vinculação dinâmica.

R .. GitHub PARE DE AJUDAR GELO
fonte
2
Agradável! Embora eu concorde que isso pareça mais sustentável, ainda não é óbvio (para mim) como eu uso a vinculação estática em cima disso. Você pode elaborar?
Manav
2
Se cada módulo tiver sua própria foo_moduleestrutura (com nomes exclusivos), você pode simplesmente criar um arquivo extra com uma matriz de struct { const char *module_name; const struct module *module_funcs; }e uma função simples para procurar nesta tabela o módulo que deseja "carregar" e retornar o ponteiro correto, e use este no lugar de dlopene dlsym.
R .. GitHub Pare de ajudar o gelo
@R .. É verdade, mas acrescenta custo de manutenção ao manter a estrutura do módulo.
user877329
3

Um exemplo moderno de onde os ponteiros de função podem diferir em tamanho dos ponteiros de dados: ponteiros de função de membro da classe C ++

Citado diretamente em https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Agora existem dois possíveis this indicadores .

Um ponteiro para uma função de membro de Base1pode ser usado como um ponteiro para uma função de membro de Derived, uma vez que ambos usam o mesmo this ponteiro. Mas um ponteiro para uma função membro de Base2não pode ser usado como está como um ponteiro para uma função membro de Derived, uma vez que othis ponteiro precisa ser ajustado.

Existem muitas maneiras de resolver isso. Veja como o compilador do Visual Studio decide lidar com isso:

Um ponteiro para uma função de membro de uma classe herdada de multiplicação é realmente uma estrutura.

[Address of function]
[Adjustor]

O tamanho de uma função de ponteiro para membro de uma classe que usa herança múltipla é o tamanho de um ponteiro mais o tamanho de a size_t.

tl; dr: Ao usar herança múltipla, um ponteiro para uma função membro pode (dependendo do compilador, versão, arquitetura, etc.) ser realmente armazenado como

struct { 
    void * func;
    size_t offset;
}

que é obviamente maior que a void *.

Andrew Sun
fonte
2

Na maioria das arquiteturas, os ponteiros para todos os tipos de dados normais têm a mesma representação; portanto, a conversão entre tipos de ponteiros de dados não é uma opção.

No entanto, é concebível que os ponteiros de função possam exigir uma representação diferente, talvez eles sejam maiores que outros ponteiros. Se o void * pudesse conter ponteiros de função, isso significaria que a representação do void * teria que ser do tamanho maior. E todas as transmissões de ponteiros de dados para / do void * teriam que executar essa cópia extra.

Como alguém mencionou, se você precisar disso, poderá obtê-lo usando um sindicato. Mas a maioria dos usos do void * é apenas para dados, portanto, seria oneroso aumentar todo o uso de memória apenas no caso de um ponteiro de função precisar ser armazenado.

Barmar
fonte
-1

Eu sei que isso não foi comentada desde 2012, mas eu pensei que seria útil acrescentar que eu faço saber uma arquitetura que tem muito ponteiros incompatíveis para dados e funções desde uma chamada em que o privilégio arquitetura cheques e carrega a informação extra. Nenhuma quantidade de elenco ajudará. É o moinho .

phorgan1
fonte
Esta resposta está errada. Você pode, por exemplo, converter um ponteiro de função em um ponteiro de dados e lê-lo (se você tiver permissões para ler esse endereço, como de costume). O resultado faz tanto sentido quanto, por exemplo, no x86.
Manuel Jacob