Dlaczego wskaźniki funkcji i wskaźniki danych są niezgodne w C / C ++?

130

Czytałem, że konwersja wskaźnika funkcji na wskaźnik danych i odwrotnie działa na większości platform, ale nie gwarantuje, że zadziała. Dlaczego tak się dzieje? Czy oba nie powinny być po prostu adresami do pamięci głównej, a zatem powinny być kompatybilne?

geksycyd
źródło
16
Niezdefiniowany w standardzie C, zdefiniowany w POSIX. Zwróć uwagę na różnicę.
ephemient
Jestem w tym trochę nowy, ale czy nie powinieneś robić obsady po prawej stronie znaku „=”? Wydaje mi się, że problem polega na tym, że przypisujesz wskaźnik do pustej przestrzeni. Ale widzę, że strona podręcznika to robi, więc mam nadzieję, że ktoś może mnie nauczyć. Widzę przykłady w 'net of people casting the return value from dlsym, np. Tutaj: daniweb.com/forums/thread62561.html
JasonWoof.
9
Zwróć uwagę, co mówi POSIX w sekcji o typach danych : §2.12.3 Typy wskaźników. Wszystkie typy wskaźników funkcji powinny mieć taką samą reprezentację jak wskaźnik typu do void. Konwersja wskaźnika funkcji na void *nie zmienia reprezentacji. void *Wartość wynikająca z takiego przekształcenia można przekształcić z powrotem do pierwotnego typu wskaźnik funkcji, za pomocą wyraźnego obsady, bez utraty informacji. Uwaga : Standard ISO C tego nie wymaga, ale jest to wymagane w celu zachowania zgodności z POSIX.
Jonathan Leffler,
2
to jest pytanie w dziale O
NAS
1
@KeithThompson: świat się zmienia - i POSIX też. To, co napisałem w 2012 roku, nie obowiązuje już w 2018 roku. Standard POSIX zmienił słownictwo. Jest teraz powiązany z dlsym()- zwróć uwagę na koniec sekcji „Użycie aplikacji”, w której jest napisane: Zauważ, że konwersja ze void *wskaźnika do wskaźnika funkcji, jak w: fptr = (int (*)(int))dlsym(handle, "my_function"); nie jest zdefiniowana w standardzie ISO C. Ten standard wymaga tej konwersji, aby działała poprawnie w zgodnych implementacjach.
Jonathan Leffler

Odpowiedzi:

172

Architektura nie musi przechowywać kodu i danych w tej samej pamięci. W architekturze Harvardu kod i dane są przechowywane w zupełnie innej pamięci. Większość architektur to architektury Von Neumanna z kodem i danymi w tej samej pamięci, ale C nie ogranicza się tylko do pewnych typów architektur, jeśli to w ogóle możliwe.

Dirk Holsopple
źródło
15
Ponadto, nawet jeśli kod i dane są przechowywane w tym samym miejscu na fizycznym sprzęcie, dostęp do oprogramowania i pamięci często uniemożliwia uruchomienie danych jako kodu bez „zgody” systemu operacyjnego. DEP i tym podobne.
Michael Graczyk
15
Co najmniej tak samo ważne jak posiadanie różnych przestrzeni adresowych (może ważniejsze) jest to, że wskaźniki funkcji mogą mieć inną reprezentację niż wskaźniki danych.
Michael Burr,
14
Nie musisz nawet mieć architektury Harvardu, aby kod i wskaźniki danych korzystały z różnych przestrzeni adresowych - zrobił to stary model pamięci DOS „Small” (blisko wskaźników z CS != DS).
kawiarnia
1
Nawet nowoczesne procesory miałyby problem z taką mieszanką, że pamięć podręczna instrukcji i danych są zwykle obsługiwane osobno, nawet jeśli system operacyjny pozwala gdzieś pisać kod.
PypeBros,
3
@EricJ. Dopóki nie zadzwonisz VirtualProtect, co pozwala oznaczyć obszary danych jako wykonywalne.
Dietrich Epp
37

Niektóre komputery mają (miały) oddzielne przestrzenie adresowe dla kodu i danych. Na takim sprzęcie to po prostu nie działa.

Język jest przeznaczony nie tylko dla obecnych aplikacji desktopowych, ale także w celu umożliwienia implementacji na dużym zestawie sprzętu.


Wygląda na to, że komitet języka C nigdy nie zamierzał void*być wskaźnikiem do działania, po prostu chciał mieć ogólny wskaźnik do obiektów.

Uzasadnienie C99 mówi:

6.3.2.3 Wskaźniki
C zostały już zaimplementowane w wielu różnych architekturach. Podczas gdy niektóre z tych architektur mają jednolite wskaźniki, które mają rozmiar pewnego typu całkowitego, maksymalnie przenośny kod nie może zakładać żadnej niezbędnej zgodności między różnymi typami wskaźników i typami całkowitymi. W niektórych implementacjach wskaźniki mogą być nawet szersze niż jakikolwiek typ liczb całkowitych.

Użycie void*(„pointer to void”) jako ogólnego typu wskaźnika do obiektu jest wynalazkiem Komitetu C89. Przyjęcie tego typu było stymulowane chęcią określenia argumentów prototypu funkcji, które albo po cichu konwertują dowolne wskaźniki (jak w fread), albo narzekają, jeśli typ argumentu nie jest dokładnie zgodny (jak w strcmp). Nie mówi się nic o wskaźnikach do funkcji, które mogą być niewspółmierne do wskaźników obiektów i / lub liczb całkowitych.

Uwaga W ostatnim akapicie nic nie jest powiedziane o wskaźnikach do funkcji . Mogą różnić się od innych wskazówek, a komisja jest tego świadoma.

Bo Persson
źródło
Standard mógłby uczynić je kompatybilnymi bez ingerowania w to, po prostu nadając typom danych ten sam rozmiar i gwarantując, że przypisanie do jednego, a następnie z powrotem, da tę samą wartość. Robią to z void *, który jest jedynym typem wskaźnika kompatybilnym ze wszystkim.
Edward Strange,
15
@CrazyEddie Nie można przypisać wskaźnika funkcji do pliku void *.
ouah
4
Mogę się mylić co do void * akceptujących wskaźniki funkcji, ale sprawa pozostaje. Bity to bity. Norma może wymagać, aby rozmiar różnych typów był w stanie pomieścić dane od siebie nawzajem, a przypisanie byłoby gwarantowane, nawet jeśli są one używane w różnych segmentach pamięci. Przyczyną tej niezgodności jest to, że NIE jest to gwarantowane przez standard, a więc dane mogą zostać utracone w przydziale.
Edward Strange,
5
Jednak wymaganie sizeof(void*) == sizeof( void(*)() )marnuje miejsce w przypadku, gdy wskaźniki funkcji i wskaźniki danych mają różne rozmiary. Był to częsty przypadek w latach 80-tych, kiedy napisano pierwszy standard C.
Robᵩ,
8
@RichardChambers: Różne przestrzenie adresowe mogą również mieć różne szerokości adresów , na przykład Atmel AVR, który używa 16 bitów na instrukcje i 8 bitów na dane; w takim przypadku byłaby trudna konwersja z danych (8-bitowych) do wskaźników funkcyjnych (16-bitowych) iz powrotem. C ma być łatwe do wdrożenia; część tej łatwości wynika z niekompatybilnych ze sobą wskaźników danych i instrukcji.
John Bode,
30

Dla tych, którzy pamiętają MS-DOS, Windows 3.1 i starsze, odpowiedź jest dość prosta. Wszystkie z nich były używane do obsługi kilku różnych modeli pamięci, z różnymi kombinacjami charakterystyk kodu i wskaźników danych.

Na przykład dla modelu Compact (mały kod, duże dane):

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

i odwrotnie w modelu Medium (duży kod, małe dane):

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

W tym przypadku nie miałeś oddzielnego miejsca na kod i datę, ale nadal nie mogłeś konwertować między dwoma wskaźnikami (bez użycia niestandardowych modyfikatorów __near i __far).

Dodatkowo nie ma gwarancji, że nawet jeśli wskaźniki są tego samego rozmiaru, wskazują na to samo - w modelu DOS Small memory zarówno kod, jak i dane używane w pobliżu wskaźników, ale wskazywały na różne segmenty. Zatem konwersja wskaźnika funkcji na wskaźnik danych nie dałaby wskaźnika, który w ogóle miałby związek z funkcją, a zatem taka konwersja nie miała sensu.

Tomek
źródło
Re: „Konwersja wskaźnika funkcji na wskaźnik danych nie dałaby wskaźnika, który w ogóle miałby związek z funkcją, a zatem taka konwersja nie miała sensu”: To nie do końca wynika. Konwersja an int*na a void*daje wskaźnik, z którym tak naprawdę nie można nic zrobić, ale nadal przydatna jest możliwość wykonania konwersji. (Dzieje się tak, ponieważ void*może przechowywać dowolny wskaźnik obiektu, więc może być używany do ogólnych algorytmów, które nie muszą wiedzieć, jaki typ przechowują. To samo mogłoby być przydatne również dla wskaźników funkcji, gdyby było to dozwolone).
ruakh
4
@ruakh: W przypadku konwersji int *do void *, void *gwarantujemy, że przynajmniej wskaże ten sam obiekt, co oryginał int *- więc jest to przydatne w przypadku ogólnych algorytmów, które mają dostęp do wskazanego obiektu, np int n; memcpy(&n, src, sizeof n);. W przypadku, gdy konwersja wskaźnika funkcji na a void *nie daje wskaźnika wskazującego na funkcję, nie jest to przydatne dla takich algorytmów - jedyną rzeczą, którą możesz zrobić, jest void *ponowne przekonwertowanie wskaźnika wstecznego na wskaźnik funkcji, więc możesz tak po prostu użyj wskaźnika unionzawierającego a void *i.
kawiarnia
@caf: W porządku. Dziękuję za zwrócenie uwagi. I o to chodzi, nawet jeśli void* sam punkt do funkcji, przypuszczam, że będzie to zły pomysł dla ludzi, aby przekazać ją memcpy. :-P
ruakh
Skopiowane z góry: Zwróć uwagę, co mówi POSIX w Typach danych : §2.12.3 Typy wskaźników. Wszystkie typy wskaźników funkcji powinny mieć taką samą reprezentację jak wskaźnik typu do void. Konwersja wskaźnika funkcji na void *nie zmienia reprezentacji. void *Wartość wynikająca z takiego przekształcenia można przekształcić z powrotem do pierwotnego typu wskaźnik funkcji, za pomocą wyraźnego obsady, bez utraty informacji. Uwaga : Norma ISO C tego nie wymaga, ale jest wymagana w celu zachowania zgodności z POSIX.
Jonathan Leffler,
@caf Gdyby to było po prostu przekazane do jakiegoś wywołania zwrotnego, które zna właściwy typ, interesuje mnie tylko bezpieczeństwo w obie strony, a nie żadna inna relacja, którą te przekonwertowane wartości mogą mieć.
Deduplicator
24

Wskaźniki do void mają być w stanie pomieścić wskaźnik do dowolnego rodzaju danych - ale niekoniecznie wskaźnik do funkcji. Niektóre systemy mają inne wymagania dotyczące wskaźników do funkcji niż wskaźniki do danych (np. Istnieją procesory DSP o różnym adresowaniu danych w porównaniu z kodem, model medium w systemie MS-DOS wykorzystywał 32-bitowe wskaźniki do kodu, ale tylko 16-bitowe wskaźniki do danych) .

Jerry Coffin
źródło
1
ale wtedy funkcja dlsym () nie powinna zwracać czegoś innego niż void *. Chodzi mi o to, że jeśli void * nie jest wystarczająco duży dla wskaźnika funkcji, czy nie mamy już fubared?
Manav,
1
@Knickerkicker: Tak, prawdopodobnie. Jeśli pamięć służy, typ powrotu z dlsym był szczegółowo omawiany, prawdopodobnie 9 lub 10 lat temu, na liście e-mailowej OpenGroup. Nie pamiętam jednak, co (jeśli cokolwiek) z tego wyszło.
Jerry Coffin
1
masz rację. To wydaje się być dość ładne (choć nieaktualne) Podsumowanie punktu.
Manav,
2
@LegoStormtroopr: Ciekawe, jak 21 osób zgadza się z pomysłem głosowania za wzrostem , ale tylko około 3 faktycznie to zrobiło. :-)
Jerry Coffin
13

Oprócz tego, co zostało już tutaj powiedziane, warto przyjrzeć się POSIX dlsym():

Norma ISO C nie wymaga, aby wskaźniki do funkcji można było rzutować tam iz powrotem na wskaźniki do danych. Rzeczywiście, standard ISO C nie wymaga, aby obiekt typu void * mógł trzymać wskaźnik do funkcji. Implementacje obsługujące rozszerzenie XSI wymagają jednak, aby obiekt typu void * mógł przechowywać wskaźnik do funkcji. Wynik konwersji wskaźnika na funkcję we wskaźnik do innego typu danych (z wyjątkiem void *) jest jednak nadal niezdefiniowany. Zauważ, że kompilatory zgodne ze standardem ISO C są zobowiązane do generowania ostrzeżenia, jeśli zostanie podjęta próba konwersji ze wskaźnika void * na wskaźnik funkcji, jak w:

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

Ze względu na opisany tutaj problem, przyszła wersja może albo dodać nową funkcję zwracającą wskaźniki funkcji, albo obecny interfejs może zostać wycofany na rzecz dwóch nowych funkcji: jednej zwracającej wskaźniki danych, a drugiej zwracającej wskaźniki funkcji.

Maxim Egorushkin
źródło
czy to oznacza, że ​​użycie dlsym do uzyskania adresu funkcji jest obecnie niebezpieczne? Czy obecnie jest na to bezpieczny sposób?
gexicide,
4
Oznacza to, że obecnie POSIX wymaga od platformy ABI, aby zarówno wskaźniki funkcji, jak i dane mogły być bezpiecznie rzutowane tam void*iz powrotem.
Maxim Egorushkin
@gexicide Oznacza to, że implementacje, które są zgodne z POSIX, rozszerzyły język, nadając zdefiniowane w implementacji znaczenie niezdefiniowanemu zachowaniu zgodnie ze standardem. Jest nawet wymieniony jako jedno z powszechnych rozszerzeń standardu C99, sekcja J.5.7 Rzuty wskaźników funkcji.
David Hammen,
1
@DavidHammen To nie jest rozszerzenie języka, a raczej nowy dodatkowy wymóg. C nie wymaga void*zgodności ze wskaźnikiem funkcji, podczas gdy POSIX tak.
Maxim Egorushkin
9

C ++ 11 ma rozwiązanie długotrwałej niezgodności między C / C ++ i POSIX w odniesieniu do dlsym(). Można użyć reinterpret_castdo konwersji wskaźnika funkcji na / ze wskaźnika danych, o ile implementacja obsługuje tę funkcję.

Ze standardu 5.2.10 ust. 8, „konwersja wskaźnika funkcji na typ wskaźnika obiektu lub odwrotnie jest obsługiwana warunkowo”. 1.3.5 definiuje „warunkowo obsługiwane” jako „konstrukcję programu, której obsługa nie jest wymagana”.

David Hammen
źródło
Można, ale nie powinno się. Kompilator zgodny z wymaganiami musi wygenerować ostrzeżenie (co z kolei powinno wywołać błąd, por -Werror.). Lepszym (i nie korzystającym z UB) rozwiązaniem jest pobranie wskaźnika do obiektu zwróconego przez dlsym(tj. void**) I przekonwertowanie go na wskaźnik do wskaźnika funkcji . Nadal zdefiniowane w ramach implementacji, ale nie powoduje już ostrzeżenia / błędu .
Konrad Rudolph
3
@KonradRudolph: Nie zgadzam się. Sformułowanie „obsługiwane warunkowo” zostało napisane specjalnie w celu umożliwienia dlsymi GetProcAddresskompilacji bez ostrzeżenia.
MSalters,
@MSalters Co masz na myśli mówiąc „nie zgadzam się”? Albo mam rację, albo się mylę. Dokumentacja dlsym wyraźnie mówi, że „kompilatory zgodne ze standardem ISO C są zobowiązane do generowania ostrzeżenia w przypadku próby konwersji ze wskaźnika void * na wskaźnik funkcji”. Nie pozostawia to wiele miejsca na spekulacje. I GCC (z -pedantic) ma ostrzec. Ponownie, żadne spekulacje nie są możliwe.
Konrad Rudolph
1
Kontynuacja: myślę, że teraz rozumiem. To nie jest UB. Jest zdefiniowany w ramach implementacji. Nadal nie jestem pewien, czy ostrzeżenie musi zostać wygenerowane, czy nie - prawdopodobnie nie. No cóż.
Konrad Rudolph
2
@KonradRudolph: Nie zgodziłem się z twoją opinią „nie należy”. W odpowiedzi konkretnie wspomniano o C ++ 11, a ja byłem członkiem C ++ CWG w czasie, gdy ten problem był rozwiązywany. C99 rzeczywiście ma inne sformułowania, obsługiwane warunkowo to wynalazek C ++.
MSalters,
7

W zależności od docelowej architektury, kod i dane mogą być przechowywane w zasadniczo niekompatybilnych, fizycznie odrębnych obszarach pamięci.

Graham Borland
źródło
„fizycznie odrębne” Rozumiem, ale czy możesz wyjaśnić bardziej szczegółowo rozróżnienie „zasadniczo niekompatybilne”. Jak powiedziałem w pytaniu, wskaźnik void nie powinien być tak duży jak jakikolwiek inny typ wskaźnika - czy też jest to błędne założenie z mojej strony.
Manav,
@KnickerKicker: void *jest wystarczająco duży, aby pomieścić dowolny wskaźnik danych, ale niekoniecznie wskaźnik funkcji.
ephemient
1
powrót do przyszłości: P
SSpoke
5

undefined niekoniecznie oznacza niedozwolone, może to oznaczać, że implementujący kompilator ma większą swobodę, aby zrobić to tak, jak chce.

Na przykład może to nie być możliwe na niektórych architekturach - undefined pozwala im nadal mieć zgodną bibliotekę „C”, nawet jeśli nie możesz tego zrobić.

Martin Beckett
źródło
5

Inne rozwiązanie:

Zakładając, że POSIX gwarantuje, że wskaźniki funkcji i danych mają ten sam rozmiar i reprezentację (nie mogę znaleźć tekstu do tego, ale cytowany przykład OP sugeruje, że przynajmniej zamierzali spełnić to wymaganie), następujące działania powinny działać:

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

Zapobiega to naruszaniu reguł aliasingu poprzez przeglądanie char []reprezentacji, która może aliasować wszystkie typy.

Jeszcze inne podejście:

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

Ale poleciłbym to memcpypodejście, jeśli chcesz absolutnie w 100% poprawne C.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
5

Mogą to być różne typy z różnymi wymaganiami dotyczącymi miejsca. Przypisanie do jednego z nich może nieodwracalnie przeciąć wartość wskaźnika, tak że przypisanie z powrotem spowoduje coś innego.

Uważam, że mogą to być różne typy, ponieważ standard nie chce ograniczać możliwych implementacji, które oszczędzają miejsce, gdy nie jest to potrzebne lub gdy rozmiar może spowodować, że procesor będzie musiał robić dodatkowe bzdury, aby go użyć itp.

Edward Strange
źródło
3

Jedynym naprawdę przenośnym rozwiązaniem jest nie używanie dlsymdo funkcji, a zamiast tego użycie dlsymdo uzyskania wskaźnika do danych, które zawierają wskaźniki funkcji. Na przykład w Twojej bibliotece:

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

a następnie w aplikacji:

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

Nawiasem mówiąc, jest to i tak dobra praktyka projektowa, która ułatwia obsługę zarówno dynamicznego ładowania przez, jak dlopeni statycznego łączenia wszystkich modułów w systemach, które nie obsługują dynamicznego łączenia, lub gdy użytkownik / integrator systemu nie chce używać dynamicznego łączenia.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
2
Ładny! Chociaż zgadzam się, że wydaje się to łatwiejsze w utrzymaniu, nadal nie jest (dla mnie) oczywiste, w jaki sposób wbijam w to statyczne linkowanie. Czy możesz rozwinąć?
Manav,
2
Jeśli każdy moduł ma własną foo_modulestrukturę (z unikalnymi nazwami), możesz po prostu utworzyć dodatkowy plik z tablicą struct { const char *module_name; const struct module *module_funcs; }i prostą funkcją, aby przeszukać tę tabelę w poszukiwaniu modułu, który chcesz "załadować" i zwrócić właściwy wskaźnik, a następnie użyj tego zamiast dlopeni dlsym.
R .. GitHub PRZESTAŃ POMÓC W LODZIE
@R .. Prawda, ale zwiększa koszty utrzymania, ponieważ trzeba zachować strukturę modułu.
user877329
3

Nowoczesny przykład, w którym wskaźniki funkcji mogą różnić się rozmiarem od wskaźników danych: wskaźniki funkcji składowych klasy C ++

Cytowane bezpośrednio z 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(); };

Istnieją teraz dwie możliwe thiswskazówki.

Wskaźnik do funkcji składowej Base1może być używany jako wskaźnik do funkcji składowej Derived, ponieważ oba używają tego samego this wskaźnika. Jednak wskaźnik do funkcji Base2składowej nie może być używany jako wskaźnik do funkcji składowej programu Derived, ponieważ this wskaźnik wymaga dostosowania.

Istnieje wiele sposobów rozwiązania tego problemu. Oto, jak kompilator programu Visual Studio decyduje się to obsłużyć:

Wskaźnik do funkcji składowej klasy dziedziczonej wielokrotnie jest w rzeczywistości strukturą.

[Address of function]
[Adjustor]

Rozmiar funkcji wskaźnika do elementu członkowskiego klasy, która korzysta z dziedziczenia wielokrotnego, to rozmiar wskaźnika plus rozmiar a size_t.

tl; dr: Podczas korzystania z dziedziczenia wielokrotnego wskaźnik do funkcji składowej może (w zależności od kompilatora, wersji, architektury itp.) W rzeczywistości być przechowywany jako

struct { 
    void * func;
    size_t offset;
}

który jest oczywiście większy niż void *.

Andrew Sun
źródło
2

Na większości architektur wskaźniki do wszystkich normalnych typów danych mają tę samą reprezentację, więc rzutowanie między typami wskaźników danych nie jest możliwe.

Jednak można sobie wyobrazić, że wskaźniki funkcji mogą wymagać innej reprezentacji, być może są większe niż inne wskaźniki. Gdyby void * mógł przechowywać wskaźniki funkcji, oznaczałoby to, że reprezentacja void * musiałaby mieć większy rozmiar. Wszystkie rzuty wskaźników danych do / z void * musiałyby wykonać tę dodatkową kopię.

Jak ktoś wspomniał, jeśli tego potrzebujesz, możesz to osiągnąć za pomocą związku. Jednak większość zastosowań void * dotyczy tylko danych, więc zwiększenie całkowitego wykorzystania pamięci na wypadek konieczności zapisania wskaźnika funkcji byłoby uciążliwe.

Barmar
źródło
-1

Wiem, że nie zostało to skomentował od 2012 roku, ale pomyślałem, że warto byłoby dodać, że zrobić znać architekturę, która ma bardzo niekompatybilne odnośniki do danych i funkcji, ponieważ rozmowy na ten przywilej sprawdza architektura i niesie dodatkowe informacje. Żadna ilość rzucania nie pomoże. To jest młyn .

phorgan1
źródło
Ta odpowiedź jest błędna. Możesz na przykład przekonwertować wskaźnik funkcji na wskaźnik danych i czytać z niego (jeśli masz uprawnienia do odczytu z tego adresu, jak zwykle). Wynik ma tyle samo sensu, co np. Na x86.
Manuel Jacob