Чому функції покажчиків та покажчиків даних несумісні в C / C ++?


130

Я читав, що перетворення покажчика функції на покажчик даних і навпаки працює на більшості платформ, але не гарантовано. Чому це так? Чи не повинні обидва бути просто адресами в основну пам'ять і тому бути сумісними?


16
Не визначено у стандарті C, визначеному в POSIX. Розумійте різницю.
ефемія

Я трохи новий у цьому, але чи не слід робити актерів у правій частині "="? Мені здається, проблема полягає в тому, що ви присвоюєте недійсний покажчик. Але я бачу, що ця сторінка робить це, тому сподіваюся, хтось може мене навчити. Я бачу приклади в "чистій кількості людей, які передають значення повернення від dlsym, наприклад, тут: daniweb.com/forums/thread62561.html
JasonWoof

9
Зверніть увагу на те, що говорить POSIX у розділі Типи даних : §2.12.3 Типи вказівників. Усі типи вказівників функцій повинні мати те саме представлення, що й покажчик типу void. Перетворення покажчика функції в void *не повинно змінювати подання. void *Значення в результаті такого перетворення може бути перетворена назад у вихідний тип покажчика функції, використовуючи явне приведення, без втрати інформації. Примітка : Стандарт ISO C цього не вимагає, але він необхідний для відповідності POSIX.
Джонатан Леффлер

2
це питання у розділі ПРО НАС на цьому веб-сайті .. :) :) Дивись, що ви тут
ZooZ

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

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

Мова розроблена не тільки для поточних настільних додатків, але і для того, щоб вона могла реалізовуватися на великому наборі обладнання.


Схоже, що мовний комітет С ніколи не мав void*на меті бути покажчиком для функціонування, вони просто хотіли загального вказівника на об’єкти.

Обґрунтування C99 говорить:

6.3.2.3 Покажчики
C тепер впроваджені для широкого кола архітектур. Хоча деякі з цих архітектур містять уніфіковані покажчики, що мають розмір певного цілого типу, максимально портативний код не може передбачати будь-якої необхідної відповідності між різними типами вказівників та цілими типами. У деяких реалізаціях вказівники можуть бути навіть ширшими за будь-який цілий тип.

Використання void*("покажчик на void") як загального типу вказівника об'єкта є винаходом Комітету C89. Прийняття цього типу стимулювалося прагненням вказати аргументи прототипу функції, які або спокійно перетворюють довільні покажчики (як у fread), або скаржаться, якщо тип аргументу точно не відповідає (як у strcmp). Про покажчики функцій нічого не сказано, що може бути невідповідним вказівникам об'єктів та / або цілим числу.

Примітка Про вказівники на функції в останньому абзаці нічого не сказано . Вони можуть відрізнятися від інших покажчиків, і комітет це знає.


Стандарт може зробити їх сумісними, не поводячись із цим, просто зробивши типи даних однакового розміру та гарантуючи, що присвоєння одному і потім назад призведе до того ж значення. Вони роблять це з void *, який є єдиним типом вказівника, сумісним із усім.
Едвард Странд

15
@CrazyEddie Ви не можете призначити покажчик функції a void *.
оуа

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

5
Але вимагати sizeof(void*) == sizeof( void(*)() )втрати місця в тому випадку, коли покажчики функцій та покажчики даних мають різний розмір. Це був звичайний випадок у 80-х, коли був написаний перший стандарт С.
Robᵩ

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

30

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

Так, наприклад, для моделі Compact (невеликий код, великі дані):

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

і навпаки в моделі "Середній" (великий код, невеликі дані):

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

У цьому випадку у вас не було окремого сховища для коду та дати, але все ще не вдалося конвертувати між двома вказівниками (окрім використання нестандартних модифікаторів __near та __far).

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


Re: "перетворення вказівника функції на покажчик даних не дасть тобі покажчика, який взагалі не мав жодного відношення до функції, а значить, для такої конверсії не було ніякого користі": Це зовсім не випливає. Перетворення int*до void*даного вам покажчика, з яким ви дійсно нічого не можете зробити, але все-таки корисно мати змогу здійснити конверсію. (Це тому, що void*може зберігати будь-який вказівник об'єкта, тому його можна використовувати для загальних алгоритмів, яким не потрібно знати, якого типу вони мають. Те ж саме може бути корисним і для покажчиків функцій, якби це було дозволено.)
ruakh

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

@caf: Досить справедливо. Дякуємо, що вказали на це. І з цього приводу, навіть якщо void* дійсно вказує на функцію, я вважаю, що це було б поганою ідеєю для людей, щоб передати її memcpy. :-P
ruakh

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

@caf Якщо він просто повинен бути переданий до якогось зворотного дзвінка, який знає належний тип, мене цікавить лише безпека в зворотному напрямку, а не будь-яке інше відношення до цих перетворених значень.
Дедуплікатор

23

Вказівники на недійсність повинні вміщувати покажчик на будь-який тип даних, але не обов'язково вказівник на функцію. Деякі системи мають інші вимоги до покажчиків функцій, ніж покажчики на дані (наприклад, є ЦСП з різною адресацією для даних проти коду; середня модель в MS-DOS використовується 32-бітні покажчики на код, але лише 16-бітні вказівники для даних) .


1
але тоді не слід, щоб функція dlsym () повертала щось інше, ніж порожнечу *. Я маю на увазі, якщо пустота * не є достатньо великою для функції вказівника, то хіба ми вже не захопилися?
Манав

1
@Knickerkicker: Так, певно. Якщо пам'ять слугує, тип повернення від dlsym обговорювався довше, ймовірно, 9 або 10 років тому, у списку електронних листів OpenGroup. Мимоволі, я не пам'ятаю, що (якщо що-небудь) вийшло з цього, хоча.
Джеррі Труну

1
ти маєш рацію. Це здається досить приємним (хоча і застарілим) підсумком вашої точки зору.
Манав


2
@LegoStormtroopr: Цікаво, як 21 людина погоджується з ідеєю проголосувати, але лише близько 3 насправді зробили це. :-)
Джеррі Коффін

13

Окрім сказаного тут, цікаво подивитися на POSIX dlsym():

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

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

Через проблему, зазначену тут, майбутня версія може або додати нову функцію для повернення функціональних покажчиків, або поточний інтерфейс може бути застарілим на користь двох нових функцій: однієї, яка повертає покажчики даних, і іншої, що повертає покажчики функцій.


чи означає це, що використання dlsym для отримання адреси функції наразі небезпечно? Наразі існує безпечний спосіб це зробити?
гексицид

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, повинні генерувати попередження, якщо спробу перетворення з пустоти * вказівник на функціональний покажчик". Це не залишає багато можливостей для спекуляцій. І GCC (з -pedantic) дійсно попереджає. Знову ж, ніяких спекуляцій неможливо.
Конрад Рудольф

1
Продовження: Я думаю, зараз я розумію. Це не UB. Це визначено реалізацією. Я все ще не впевнений, чи повинно бути попередження, чи ні - можливо, ні. Що ж, добре.
Конрад Рудольф

2
@KonradRudolph: Я не погодився з вашим "не слід", що є думкою. У відповіді конкретно згадувалося C ++ 11, і я був членом CW + CG у той час, коли було вирішено питання. C99 дійсно має різні формулювання, умовно підтримувані є винаходом C ++.
MSalters

7

Залежно від цільової архітектури, код і дані можуть зберігатися у принципово несумісних, фізично відмінних областях пам'яті.


"фізично різний", я розумію, але чи можете ви детальніше зупинитися на "принципово несумісній" різниці. Як я вже говорив у запитанні, чи не пустий покажчик повинен бути таким же великим, як будь-який тип вказівника, - чи це неправильна презумпція з мого боку.
Манав

@KnickerKicker: void *досить великий, щоб вмістити будь-який вказівник даних, але не обов'язково будь-який вказівник функції.
ефемія

1
назад у майбутнє: P
SSpoke

5

undefined не обов'язково означає, що не дозволено, це може означати, що у виконавця компілятора є більше свободи робити це так, як вони хочуть.

Наприклад, це може бути неможливим для деяких архітектур - невизначений дозволяє їм все ще мати відповідну бібліотеку "C", навіть якщо ви не можете цього зробити.


5

Ще одне рішення:

Якщо припустити, що POSIX гарантує, що функції та покажчики даних мають однаковий розмір і представлення (я не можу знайти текст для цього, але приведений приклад ОП дозволяє припустити, що вони принаймні мають на меті висунути цю вимогу), слід працювати наступним чином:

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
Приємно! Хоча я згоден, це здається більш реальним, для мене все ще не очевидно, як я забиваюсь статичним зв'язком поверх цього. Чи можете ви докладно?
Манав

2
Якщо кожен модуль має власну foo_moduleструктуру (з унікальними іменами), ви можете просто створити додатковий файл з масивом struct { const char *module_name; const struct module *module_funcs; }та простою функцією для пошуку в цій таблиці модуля, який ви хочете "завантажити" та повернути потрібний вказівник, а потім скористайтеся цим замість dlopenі dlsym.
R .. GitHub СТОП ДОПОМОГАТИ

@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]

Розмір функції вказівника на члена класу, який використовує множинне успадкування, є розміром покажчика плюс розмір a 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.