Почему указатели на функции и указатели данных несовместимы в C / C ++?


130

Я читал, что преобразование указателя функции в указатель данных и наоборот работает на большинстве платформ, но не гарантируется. Почему это так? Разве оба не должны быть просто адресами в основной памяти и, следовательно, быть совместимыми?


16
Не определено в стандарте C, определено в POSIX. Обратите внимание на разницу.
ephemient

Я немного новичок в этом, но разве вы не должны делать приведение справа от знака "="? Мне кажется, проблема в том, что вы назначаете указатель void. Но я вижу, что справочная страница делает это, так что, надеюсь, кто-нибудь сможет меня обучить. Я вижу примеры в сети людей, приводящих
JasonWoof

9
Обратите внимание на то, что говорится в POSIX в разделе « Типы данных» : §2.12.3 Типы указателей. Все типы указателей функций должны иметь то же представление, что и указатели типа void. Преобразование указателя функции в void *не должно изменять представление. void *Значение в результате такого преобразования может быть преобразована обратно в исходный тип указателя функции, используя явное приведение, без потери информации. Примечание . Стандарт ISO C этого не требует, но он необходим для соответствия POSIX.
Джонатан Леффлер,

2
это вопрос в разделе О
НАС

1
@KeithThompson: мир меняется - и POSIX тоже. То, что я написал в 2012 году, больше не применимо в 2018 году. Стандарт POSIX изменил многословие. Теперь он связан с dlsym()- обратите внимание на конец раздела «Использование приложения», где говорится: Обратите внимание, что преобразование из void *указателя в указатель функции, как в:, fptr = (int (*)(int))dlsym(handle, "my_function"); не определено стандартом ISO C. Этот стандарт требует, чтобы это преобразование работало правильно в соответствующих реализациях.
Джонатан Леффлер

Ответы:


171

Архитектура не обязана хранить код и данные в одной и той же памяти. В гарвардской архитектуре код и данные хранятся в совершенно другой памяти. Большинство архитектур - это архитектуры фон Неймана с кодом и данными в одной и той же памяти, но C не ограничивается только определенными типами архитектур, если это вообще возможно.


15
Кроме того, даже если код и данные хранятся в одном и том же месте на физическом оборудовании, доступ к программному обеспечению и памяти часто препятствует запуску данных в виде кода без «одобрения» операционной системы. DEP и тому подобное.
Michael Graczyk

15
По крайней мере, так же важно, как наличие разных адресных пространств (может быть, более важно), что указатели функций могут иметь другое представление, чем указатели данных.
Майкл Берр,

14
Вам даже не обязательно иметь гарвардскую архитектуру, чтобы иметь указатели на код и данные, использующие разные адресные пространства - это делала старая модель памяти DOS "Small" (рядом с указателями с CS != DS).
caf,

1
даже современные процессоры столкнутся с такой смесью, поскольку кэш инструкций и данных обычно обрабатываются отдельно, даже если операционная система позволяет вам где-то писать код.
PypeBros

3
@EricJ. Пока вы не позвоните VirtualProtect, что позволяет вам отмечать области данных как исполняемые.
Дитрих Эпп

37

Некоторые компьютеры имеют (имели) отдельные адресные пространства для кода и данных. На таком оборудовании просто не работает.

Этот язык разработан не только для текущих настольных приложений, но и позволяет реализовать его на большом количестве оборудования.


Похоже, что комитет по языку C никогда не намеревался void*быть указателем на функцию, им просто нужен общий указатель на объекты.

Обоснование C99 гласит:

6.3.2.3 Указатели
C теперь реализованы на широком спектре архитектур. В то время как некоторые из этих архитектур имеют единообразные указатели размером с некоторый целочисленный тип, максимально переносимый код не может предполагать какое-либо необходимое соответствие между различными типами указателей и целочисленными типами. В некоторых реализациях указатели могут быть даже шире любого целочисленного типа.

Использование void*(«указатель на void») в качестве универсального типа указателя объекта является изобретением Комитета C89. Принятие этого типа было стимулировано желанием указать аргументы прототипа функции, которые либо незаметно преобразуют произвольные указатели (как в fread), либо жалуются, если тип аргумента не совпадает в точности (как в strcmp). Ничего не сказано об указателях на функции, которые могут быть несоизмеримы с указателями на объекты и / или целыми числами.

Примечание. В последнем абзаце ничего не говорится об указателях на функции . Они могут отличаться от других указателей, и комитет это знает.


Стандарт может сделать их совместимыми, не вмешиваясь в это, просто сделав типы данных одинакового размера и гарантируя, что присвоение одному, а затем обратно приведет к тому же значению. Они делают это с помощью void *, который является единственным типом указателя, совместимым со всем.
Эдвард Стрэндж

15
@CrazyEddie Вы не можете назначить указатель на функцию для void *.
Оуа,

4
Я мог ошибаться, говоря, что void * принимает указатели на функции, но суть остается. Биты есть биты. Стандарт может требовать, чтобы размер разных типов мог умещать данные друг от друга, и назначение гарантированно работало бы, даже если они используются в разных сегментах памяти. Причина, по которой существует эта несовместимость, заключается в том, что это НЕ гарантируется стандартом, и поэтому данные могут быть потеряны при назначении.
Эдвард Стрэндж

5
Но это sizeof(void*) == sizeof( void(*)() )приведет к потере места в случае, когда указатели функций и указатели данных имеют разные размеры. Это было обычным делом в 80-х, когда был написан первый стандарт C.
Robᵩ

8
@RichardChambers: разные адресные пространства также могут иметь разную ширину адресов , например, Atmel AVR, который использует 16 бит для инструкций и 8 бит для данных; в этом случае было бы сложно преобразовать указатели данных (8 бит) в указатели функций (16 бит) и обратно. C должно быть легко реализовать; Часть этой легкости происходит из-за того, что данные и указатели инструкций остаются несовместимыми друг с другом.
Джон Боде

30

Для тех, кто помнит MS-DOS, Windows 3.1 и старше, ответ довольно прост. Все они используются для поддержки нескольких различных моделей памяти с различными комбинациями характеристик для кода и указателей данных.

Так, например, для модели Compact (небольшой код, большие данные):

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

и наоборот, в модели Medium (большой код, небольшие данные):

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

В этом случае у вас не было отдельного хранилища для кода и даты, но вы все равно не могли конвертировать между двумя указателями (за исключением использования нестандартных модификаторов __near и __far).

Кроме того, нет гарантии, что даже если указатели имеют одинаковый размер, они указывают на одно и то же - в модели памяти DOS Small и код, и данные используются рядом с указателями, но они указывают на разные сегменты. Таким образом, преобразование указателя функции в указатель данных не даст вам указатель, который вообще имеет какое-либо отношение к функции, и, следовательно, такое преобразование было бесполезно.


Re: «преобразование указателя функции в указатель данных не даст вам указатель, который вообще имеет какое-либо отношение к функции, и, следовательно, не было никакого смысла в таком преобразовании»: это не совсем так. Преобразование int*в a void*дает вам указатель, с которым вы действительно ничего не можете сделать, но все же полезно иметь возможность выполнить преобразование. (Это потому, что void*может хранить любой указатель на объект, поэтому его можно использовать для общих алгоритмов, которым не нужно знать, какой тип они хранят. То же самое могло бы быть полезно и для указателей на функции, если бы это было разрешено.)
ruakh

4
@ruakh: В случае преобразуя int *к void *, то void *гарантируется , по крайней мере , указывают на тот же объект , как оригинал int *сделал - так что это полезно для общих алгоритмов, доступ заостренный к объекту, например int n; memcpy(&n, src, sizeof n);. В случае, когда преобразование указателя функции в a void *не дает указателя, указывающего на функцию, это бесполезно для таких алгоритмов - единственное, что вы могли бы сделать, это void *снова преобразовать обратно в указатель функции, поэтому вы можете как ну просто используйте указатель, unionсодержащий void *и функцию.
caf,

@caf: Достаточно честно. Спасибо что подметил это. И в этом отношении, даже если бы void* действительно указывал на функцию, я полагаю, людям было бы плохой идеей передавать ее memcpy. :-P
ruakh

Скопировано сверху: Обратите внимание на то, что POSIX говорит в Типах данных : §2.12.3 Типы указателей. Все типы указателей функций должны иметь то же представление, что и указатели типа void. Преобразование указателя функции в void *не должно изменять представление. void *Значение в результате такого преобразования может быть преобразована обратно в исходный тип указателя функции, используя явное приведение, без потери информации. Примечание . Стандарт ISO C этого не требует, но он необходим для соответствия POSIX.
Джонатан Леффлер,

@caf Если он просто должен быть передан некоторому обратному вызову, который знает правильный тип, меня интересует только безопасность туда и обратно, а не какие-либо другие отношения, которые могут иметь эти преобразованные значения.
Дедупликатор

23

Предполагается, что указатели на void могут содержать указатель на любые данные, но не обязательно указатель на функцию. В некоторых системах требования к указателям на функции отличаются от требований к указателям на данные (например, существуют DSP с разной адресацией для данных и кода, в средней модели в MS-DOS используются 32-разрядные указатели для кода, но только 16-разрядные указатели для данных) ,


1
но тогда функция dlsym () не должна возвращать что-то кроме void *. Я имею в виду, что если void * недостаточно велик для указателя функции, разве мы уже не справились?
Manav

1
@Knickerkicker: Да, наверное. Если мне не изменяет память, возвращаемый тип dlsym подробно обсуждался, вероятно, 9 или 10 лет назад в списке рассылки OpenGroup. Навскидку, я не помню, что (если что) из этого вышло.
Джерри Коффин,

1
ты прав. Это кажется довольно хорошим (хотя и устаревшим) изложением вашей точки зрения.
Manav


2
@LegoStormtroopr: Интересно, как 21 человек согласен с идеей повышающего голосования, но только около 3 на самом деле сделали это. :-)
Джерри Коффин

13

Помимо того, что здесь уже сказано, интересно посмотреть на POSIX dlsym():

Стандарт ISO C не требует, чтобы указатели на функции могли быть преобразованы туда и обратно к указателям на данные. Действительно, стандарт ISO C не требует, чтобы объект типа void * мог содержать указатель на функцию. Однако реализации, поддерживающие расширение XSI, действительно требуют, чтобы объект типа void * мог содержать указатель на функцию. Однако результат преобразования указателя на функцию в указатель на другой тип данных (кроме void *) все еще не определен. Обратите внимание, что компиляторы, соответствующие стандарту ISO C, должны генерировать предупреждение, если предпринимается попытка преобразования из указателя void * в указатель функции, как в:

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

Из-за проблемы, отмеченной здесь, будущая версия может либо добавить новую функцию для возврата указателей функций, либо текущий интерфейс может быть устаревшим в пользу двух новых функций: одна возвращает указатели данных, а другая - указатели функций.


означает ли это, что использование dlsym для получения адреса функции в настоящее время небезопасно? Есть ли в настоящее время безопасный способ сделать это?
gexicide

4
Это означает, что в настоящее время POSIX требует от ABI платформы, чтобы указатели на функции и данные могли быть безопасно преобразованы в void*и обратно.
Максим Егорушкин

@gexicide Это означает, что реализации, совместимые с POSIX, сделали расширение языка, придав определенное реализацией значение тому, что является неопределенным поведением в соответствии со стандартом. Он даже указан как одно из распространенных расширений стандарта C99, раздел J.5.7 Приведение указателей функций.
Дэвид Хаммен,

1
@DavidHammen Это не расширение языка, а новое дополнительное требование. C не требует void*совместимости с указателем на функцию, тогда как POSIX требует.
Максим Егорушкин

9

В C ++ 11 есть решение давнего несоответствия между C / C ++ и POSIX в отношении dlsym(). Можно использовать reinterpret_castдля преобразования указателя функции в / из указателя данных, если реализация поддерживает эту функцию.

Из стандарта, п. 5.2.10 п. 8, «условно поддерживается преобразование указателя функции в тип указателя объекта или наоборот». 1.3.5 определяет «условно поддерживаемую» как «конструкцию программы, которую реализация не обязана поддерживать».


Можно, но нельзя. Соответствующий компилятор должен генерировать для этого предупреждение (которое, в свою очередь, должно вызывать ошибку, см. -Werror). Лучшее (и не относящееся к UB) решение - получить указатель на объект, возвращаемый dlsym(т.е. void**), и преобразовать его в указатель на указатель функции . По-прежнему определяется реализацией, но больше не вызывает предупреждения / ошибки .
Конрад Рудольф

3
@KonradRudolph: Не согласен. Формулировка "условно поддерживаемая" была специально написана для того, чтобы разрешить dlsymи GetProcAddressкомпилировать без предупреждения.
MSalters

@MSalters Что значит «не согласен»? Либо я прав, либо нет. В документации dlsym прямо говорится, что «компиляторы, соответствующие стандарту ISO C, должны генерировать предупреждение при попытке преобразования из указателя void * в указатель на функцию». Это не оставляет места для домыслов. И НКУ (с -pedantic) делает предупредить. Опять же, никаких предположений невозможно.
Конрад Рудольф

1
Продолжение: думаю, теперь я понимаю. Это не УБ. Это определяется реализацией. Я до сих пор не уверен, нужно ли генерировать предупреждение - возможно, нет. Ну что ж.
Конрад Рудольф

2
@KonradRudolph: Я не согласен с вашим мнением "не следует". В ответе конкретно упоминается C ++ 11, и я был членом CWG по C ++ в то время, когда проблема решалась. C99 действительно имеет другую формулировку, условно поддерживаемое изобретение C ++.
MSalters

7

В зависимости от целевой архитектуры код и данные могут храниться в принципиально несовместимых, физически различных областях памяти.


«физически отличное» я понимаю, но не могли бы вы подробнее рассказать о «принципиально несовместимом» различии. Как я уже сказал в вопросе, это не пустой указатель, который должен иметь такой же размер, как любой тип указателя, или это неправильное предположение с моей стороны.
Манав

@KnickerKicker: void *достаточно большой, чтобы содержать любой указатель данных, но не обязательно указатель на функцию.
ephemient

1
назад в будущее: P
SSpoke 06

5

undefined не обязательно означает «не разрешено», это может означать, что разработчик компилятора имеет больше свободы делать это так, как они хотят.

Например, это может быть невозможно на некоторых архитектурах - undefined позволяет им по-прежнему иметь соответствующую библиотеку C, даже если вы не можете этого сделать.


5

Другое решение:

Предполагая, что POSIX гарантирует, что указатели функций и данных имеют одинаковый размер и представление (я не могу найти текст для этого, но приведенный пример OP предполагает, что они, по крайней мере, намеревались выполнить это требование), следующее должно работать:

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

Это позволяет избежать нарушения правил создания псевдонимов путем прохождения char []представления, которому разрешено использовать псевдонимы для всех типов.

Еще один подход:

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

Но я бы порекомендовал этот memcpyподход, если вы хотите абсолютно 100% правильного C.


5

Они могут быть разных типов с разной занимаемой площадью. Присвоение одному может необратимо сократить значение указателя, так что возвращение приведет к чему-то другому.

Я считаю, что они могут быть разных типов, потому что стандарт не хочет ограничивать возможные реализации, которые экономят место, когда оно не нужно или когда размер может привести к тому, что ЦП будет вынужден делать лишнюю ерунду для его использования и т. Д.


3

Единственное действительно переносимое решение - не использовать dlsymдля функций, а вместо этого использовать dlsymдля получения указателя на данные, содержащие указатели на функции. Например, в вашей библиотеке:

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

а затем в вашем приложении:

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

Между прочим, это в любом случае хорошая практика проектирования, которая позволяет легко поддерживать как динамическую загрузку, так dlopenи статическое связывание всех модулей в системах, которые не поддерживают динамическое связывание или где пользователь / системный интегратор не хочет использовать динамическое связывание.


2
Ницца! Хотя я согласен, что это кажется более удобным в обслуживании, все еще не очевидно (для меня), как я забиваю статические ссылки поверх этого. Вы можете уточнить?
Manav

2
Если каждый модуль имеет свою собственную foo_moduleструктуру (с уникальными именами), вы можете просто создать дополнительный файл с массивом struct { const char *module_name; const struct module *module_funcs; }и простой функцией для поиска в этой таблице модуля, который вы хотите «загрузить», и возврата правильного указателя, а затем использовать это вместо dlopenи dlsym.
R .. GitHub НЕ ПОМОГАЕТ ICE

@R .. Верно, но это увеличивает стоимость обслуживания за счет необходимости поддерживать структуру модуля.
user877329

3

Современный пример того, где указатели функций могут отличаться по размеру от указателей данных: указатели функций-членов класса C ++

Прямая цитата из 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(); };

Теперь есть два возможных thisуказателя.

Указатель на функцию-член Base1может использоваться как указатель на функцию-член Derived, поскольку они оба используют один и тот же this указатель. Но указатель на функцию-член Base2не может использоваться как есть в качестве указателя на функцию-член Derived, поскольку this указатель необходимо настроить.

Есть много способов решить эту проблему. Вот как компилятор Visual Studio решает справиться с этим:

Указатель на функцию-член многократно унаследованного класса на самом деле является структурой.

[Address of function]
[Adjustor]

Размер указателя на функцию-член класса, использующего множественное наследование, равен размеру указателя плюс размер size_t.

tl; dr: при использовании множественного наследования указатель на функцию-член может (в зависимости от компилятора, версии, архитектуры и т. д.) фактически храниться как

struct { 
    void * func;
    size_t offset;
}

что, очевидно, больше a void *.


2

На большинстве архитектур указатели на все обычные типы данных имеют одинаковое представление, поэтому преобразование типов указателей данных не выполняется.

Однако вполне возможно, что указатели функций могут потребовать другого представления, возможно, они больше, чем другие указатели. Если void * может содержать указатели на функции, это будет означать, что представление void * должно быть большего размера. И все приведения указателей данных в / из void * должны будут выполнять эту дополнительную копию.

Как кто-то сказал, если вам это нужно, вы можете добиться этого с помощью союза. Но в большинстве случаев void * используется только для данных, поэтому было бы обременительно увеличивать объем используемой ими памяти на тот случай, если необходимо сохранить указатель на функцию.


-1

Я знаю , что это не было прокомментировано с 2012 года, но я подумал , что было бы полезно добавить , что я сделать знаю архитектуру , которая имеет очень несовместимые указатели для данных и функций , так как вызов на этой архитектуру проверяет привилегию и несет дополнительную информацию. Никакой кастинг не поможет. Это мельница .


Это неверный ответ. Вы можете, например, преобразовать указатель функции в указатель данных и прочитать из него (если у вас есть разрешения на чтение с этого адреса, как обычно). Результат имеет такой же смысл, как, например, на x86.
Мануэль Джейкоб
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.