Запитуючи про загальну невизначену поведінку у С , люди іноді посилаються на суворе правило злучення.
Про що вони говорять?
Запитуючи про загальну невизначену поведінку у С , люди іноді посилаються на суворе правило злучення.
Про що вони говорять?
Відповіді:
Типова ситуація, коли ви стикаєтесь із суворими проблемами згладжування, - це накладання структури (наприклад, msg пристрою / мережі) на буфер розміру слова вашої системи (наприклад, вказівник на uint32_t
s або uint16_t
s). Якщо ви накладете структуру на такий буфер або буфер на таку структуру за допомогою введення покажчика, ви можете легко порушити суворі правила псевдоніму.
Тож у такому налаштуванні, якщо я хочу надіслати повідомлення на щось, мені доведеться мати два несумісні вказівники, що вказують на один і той же фрагмент пам'яті. Я можу тоді наївно кодувати щось подібне (у системі з sizeof(int) == 2
):
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
Правило суворого псевдоніму робить цю установку незаконною: перенаправлення покажчика, який псевдонімує об'єкт, який не є сумісним типом, або одного з інших типів, дозволеного пунктом 7 C 2011 6.5, пункт 1 1 1, є невизначеною поведінкою. На жаль, ви все ще можете кодувати таким чином, можливо, отримайте деякі попередження , нехай він складеться добре, лише щоб мати дивну несподівану поведінку під час запуску коду.
(GCC виглядає дещо непослідовною у своїй здатності давати потворні попередження, іноді попереджаючи нас доброзичливо, а іноді ні.)
Щоб зрозуміти, чому така поведінка не визначена, ми повинні подумати про те, що суворе правило суворого псевдоніму купує компілятор. В основному, з цим правилом, не потрібно думати про вставлення інструкцій для оновлення вмісту buff
кожного запуску циклу. Натомість, під час оптимізації, з деякими набридливо невимушеними припущеннями про псевдонім, він може опускати ці вказівки, завантажувати buff[0]
і buff[1
] в регістри процесора один раз до запуску циклу та пришвидшити тіло циклу. Перед тим, як було введено суворе псевдонім, компілятору довелося жити в стані параної, який вміст buff
будь-кого міг міняти в будь-який час. Таким чином, щоб отримати додаткові переваги в продуктивності та припускаючи, що більшість людей не набирають покажчиків, було введено суворе правило псевдоніму.
Майте на увазі, якщо ви вважаєте, що приклад надуманий, це може навіть статися, якщо ви передаєте буфер іншій функції, яка виконує надсилання для вас, якщо натомість у вас є.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
І переписав наш раніше цикл, щоб скористатися цією зручною функцією
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Компілятор може або не може бути або досить розумним, щоб спробувати вбудувати SendMessage, і він може, а може і не вирішити завантажувати або не завантажувати баф знову. Якщо SendMessage
є частиною іншого API, який складається окремо, він, ймовірно, має інструкції щодо завантаження вмісту буфа. Знову ж таки, можливо, ви перебуваєте в C ++, і це лише деяка шаблонна реалізація заголовка, яку компілятор вважає, що вона може вбудовуватися. А може, це просто щось, що ви написали у вашому файлі .c для власної зручності. У будь-якому випадку невизначена поведінка може все-таки настати. Навіть коли ми знаємо, що відбувається під кришкою, це все ж є порушенням правила, тому жодна чітко визначена поведінка не гарантована. Тож просто введення функції, яка займає буфер з обмеженим словом, не обов'язково допомагає.
То як мені це обійти?
Використовуйте союз. Більшість компіляторів підтримують це, не нарікаючи на суворий псевдонім. Це дозволено в C99 і явно дозволено в C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Ви можете вимкнути суворий псевдонім у вашому компіляторі ( f [no-] строгий псевдонім у gcc))
Ви можете використовувати char*
для згладжування замість слова вашої системи. Правила допускають виняток для char*
(включаючи signed char
та unsigned char
). Завжди вважається, що char*
псевдоніми інших типів. Однак це не буде працювати інакше: немає припущення, що ваша структура псевдонімує буфер символів.
Початківці остерігайтесь
Це лише одне потенційне мінне поле при накладанні двох типів один на одного. Ви також повинні дізнатися про витривалість , вирівнювання слів та про те, як правильно вирішувати проблеми вирівнювання через структури упаковки .
1 Типи, до яких C 2011 6.5 7 дозволяє дозволити lvalue, це:
unsigned char*
бути використаний далеко char*
замість цього? Я, як правило, використовую, unsigned char
а не char
як базовий тип, byte
тому що мої байти не підписані, і я не хочу, щоб дивацтво підписаної поведінки (особливо Wrt переповнювалося)
unsigned char *
нормально.
uint32_t* buff = malloc(sizeof(Msg));
та наступні unsigned int asBuffer[sizeof(Msg)];
буферні декларації об'єднання матимуть різний розмір, і жодне з них не є правильним. malloc
Виклик покладається на вирівнюванні 4 байта під капотом (не робити) , а об'єднання буде в 4 рази більше , ніж це повинно бути ... Я розумію , що це для ясності , але це помилка мені ні-the менше ...
Найкраще пояснення, яке я знайшов, - Майк Актон, Розуміння суворого згладжування . Він трохи зосереджений на розробці PS3, але це в основному лише GCC.
Зі статті:
"Строгий псевдонім - це припущення, зроблене компілятором C (або C ++), що перенаправлення покажчиків на об'єкти різних типів ніколи не буде посилатися на одне і те ж місце пам'яті (тобто псевдонім один одного.)"
Отже, якщо у вас є int*
вказівка на деяку пам'ять, що містить, int
а потім ви вказуєте a float*
на цю пам'ять і використовуєте її як float
ви порушите правило. Якщо ваш код цього не поважає, оптимізатор компілятора, швидше за все, порушить ваш код.
Виняток із правила - це a char*
, якому дозволено вказувати на будь-який тип.
Це суворе правило псевдоніму, яке знайдено в розділі 3.10 стандарту C ++ 03 (інші відповіді дають хороші пояснення, але жодна не передбачає самого правила):
Якщо програма намагається отримати доступ до збереженого значення об'єкта через значення, яке відрізняється від одного з наступних типів, поведінка не визначена:
- динамічний тип об'єкта,
- версія, що відповідає кваліфікації динамічного типу об'єкта,
- тип, який є типовим або непідписаним типом, що відповідає динамічному типу об'єкта,
- тип, який є типовим або непідписаним типом, що відповідає версії, що відповідає кваліфікації cv, динамічного типу об'єкта,
- сукупний або союзний тип, який включає один із вищезазначених типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься),
- тип, який є типовим базовим класом (можливо, cv) динамічного типу об'єкта,
- а
char
абоunsigned char
тип.
Формулювання C ++ 11 та C ++ 14 (наголошено на змінах):
Якщо програма намагається отримати доступ до збереженого значення об'єкта через glvalue іншого, ніж одного з наступних типів, поведінка не визначена:
- динамічний тип об'єкта,
- версія, що відповідає кваліфікації динамічного типу об'єкта,
- тип, подібний (як визначено в 4.4) динамічному типу об'єкта,
- тип, який є типовим або непідписаним типом, що відповідає динамічному типу об'єкта,
- тип, який є типовим або непідписаним типом, що відповідає версії, що відповідає кваліфікації cv, динамічного типу об'єкта,
- сукупний або об'єднаний тип, який включає один із вищезазначених типів серед його елементів або нестатичних членів даних (включаючи, рекурсивно, елемент або нестатичний член даних субагрегату або міститься в об'єднанні),
- тип, який є типовим базовим класом (можливо, cv) динамічного типу об'єкта,
- а
char
абоunsigned char
тип.
Дві зміни були невеликими: glvalue замість lvalue та уточнення сукупного / об'єднаного випадку.
Третя зміна дає більш надійну гарантію (послаблює чітке правило псевдоніму): Нова концепція подібних типів , які тепер безпечні для псевдоніму.
Також формулювання C (C99; ISO / IEC 9899: 1999 6.5 / 7; точне ж формулювання використовується в ISO / IEC 9899: 2011, § 6.5 ¶7):
Об'єкт має збережене значення, доступ до якого має лише вираження значення, яке має один із наступних типів 73) або 88) :
- тип, сумісний з ефективним типом об'єкта,
- кваліфікована версія типу, сумісна з ефективним типом об'єкта,
- тип, який є типовим або непідписаним типом, що відповідає ефективному типу об'єкта,
- тип, який є типовим або неподписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,
- сукупний або союзний тип, який включає один із вищезгаданих типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься), або
- тип символів
73) або 88) Метою цього переліку є конкретизація тих обставин, за яких об'єкт може бути, а може і не бути відчуженим.
wow(&u->s1,&u->s2)
він повинен бути легальним навіть тоді, коли вказівник використовується для зміни u
, і це заперечує більшість оптимізацій, що Правило псевдоніму було розроблено для полегшення.
Це уривок з мого "Що таке правило суворого відчуження і чому нас хвилює?" докладно описувати.
У псевдонімі C і C ++ пов'язане з типом виразів, яким ми можемо отримувати доступ до збережених значень. І в C, і в C ++ стандарт вказує, які типи виразів дозволені для псевдонімів, які типи. Компілятор і оптимізатор дозволяють припустити, що ми суворо дотримуємось правил псевдонімування, звідси і термін суворе правило псевдоніму . Якщо ми намагаємося отримати доступ до значення за допомогою типу не дозволеного, це класифікується як невизначена поведінка ( UB ). Після того, як ми не визначимо поведінку, всі ставки будуть вимкнені, результати нашої програми вже не є надійними.
На жаль, із суворими порушеннями дозволу ми часто отримуємо очікувані результати, залишаючи можливість майбутній варіант компілятора з новою оптимізацією зламати код, який ми вважали дійсним. Це небажано, і важливою є мета зрозуміти суворі правила дозволу та як уникнути їх порушення.
Щоб дізнатися більше про те, чому нас хвилює, ми обговоримо проблеми, які виникають при порушенні суворих правил згладжування, введіть покарання, оскільки загальні методи, що застосовуються при типовому покаранні, часто порушують суворі правила псевдоніму та як правильно вводити каламбур.
Давайте розглянемо кілька прикладів, тоді ми можемо поговорити про те, що саме кажуть стандарт (и), вивчимо декілька подальших прикладів, а потім подивимось, як уникнути суворого псевдоніму та виявити порушення, які ми пропустили. Ось приклад, який не повинен дивуватись ( живий приклад ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
У нас є int *, що вказує на пам'ять, яку займає int, і це дійсний псевдонім. Оптимізатор повинен припустити, що призначення через ip можуть оновити значення, яке займає x .
Наступний приклад показує згладжування, яке призводить до невизначеної поведінки ( живий приклад ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
У функції foo ми беремо int * і float * , у цьому прикладі ми викликаємо foo і встановлюємо обидва параметри, щоб вказувати на те саме місце пам'яті, яке в цьому прикладі містить int . Зауважте, reinterpret_cast вказує компілятору трактувати вираз так, як ніби він має тип, визначений параметром шаблону. У цьому випадку ми кажемо йому, що він має відношення до виразу & x так, як якщо б він мав тип float * . Ми можемо наївно очікувати, що результат другої кути буде 0, але з увімкненою оптимізацією за допомогою -O2 і gcc, і кланг дають такий результат:
0
1
Що може бути не очікуваним, але цілком справедливо, оскільки ми посилалися на невизначену поведінку. Поплавок не може законним чином псевдонім з Int об'єкта. Тому оптимізатор може припустити, що константа 1, що зберігається при перенаправлення i, буде значенням повернення, оскільки зберігання через f не могло би вплинути на об'єкт int . Підключення коду до Провідника компілятора показує, що це саме те, що відбувається ( живий приклад ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Оптимізатор, що використовує типовий аналіз псевдонімів (TBAA), передбачає, що 1 буде повернуто і безпосередньо переміщує постійне значення в регістр eax, який несе повернене значення. TBAA використовує мовні правила щодо того, які типи дозволені для псевдоніму, щоб оптимізувати навантаження та магазини. У цьому випадку TBAA знає, що поплавок не може псевдонімувати і int, і оптимізує подальше навантаження i .
Що саме в стандарті йдеться про те, що нам дозволено і не дозволяють робити це? Стандартна мова не є простою, тому для кожного елемента я спробую навести приклади коду, що демонструє значення.
Стандарт C11 в розділі 6.5, параграф 7, виражає наступне :
Об'єкт має збережене значення, доступ до якого має лише вираз lvalue, який має один із таких типів: 88) - тип, сумісний з ефективним типом об'єкта,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- кваліфікована версія типу, сумісна з ефективним типом об'єкта,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- тип, який є типовим або неподписаним типом, що відповідає ефективному типу об'єкта,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang має розширення, а також дозволяє призначати неподписаний int * до int *, хоча вони не сумісні типи.
- тип, який є типовим або непідписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- сукупний або союзний тип, який включає один із вищезгаданих типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься), або
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- тип символів.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Проект стандарту C ++ 17 у параграфі 11 розділу [basic.lval] говорить:
Якщо програма намагається отримати доступ до збереженого значення об'єкта через glvalue іншого, ніж одного з наступних типів, поведінка не визначена: 63 (11.1) - динамічний тип об'єкта,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - версія для динамічного типу об'єкта, кваліфікована cv,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - тип, подібний (визначений у 7.5) динамічному типу об'єкта,
(11.4) - тип, який є підписаним або непідписаним типом, що відповідає динамічному типу об'єкта,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - тип, який є типовим або непідписаним типом, що відповідає версії cv-кваліфікації динамічного типу об'єкта,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - сукупний або об'єднаний тип, який включає один із вищезазначених типів серед його елементів або нестатичних членів даних (включаючи, рекурсивно, елемент або нестатичний член даних субагрегата або міститься в об'єднанні),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - тип, який є типовим базовим класом динамічного типу об'єкта (можливо, що відповідає кваліфікації cv),
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - тип char, непідписаний char або std :: byte.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Варто зазначити, що підписані знаки не включені до списку вище, це помітна відмінність від C, яка говорить про тип символу .
Ми дійшли до цього моменту і нам може бути цікаво, чому ми хотіли б псевдонім? Відповідь, як правило, вводить каламбур , часто використовувані методи порушують суворі правила дозволу.
Іноді ми хочемо обійти систему типів і інтерпретувати об'єкт як інший тип. Це називається типом покарання , щоб повторно інтерпретувати сегмент пам'яті як інший тип. Введення покарань корисно для завдань, які хочуть отримати доступ до базового зображення об’єкта для перегляду, транспортування чи маніпулювання. Типові області, за якими ми вважаємо, що застосовується тип покарання, - це компілятори, серіалізація, мережевий код тощо ...
Традиційно це досягається шляхом взяття адреси об’єкта, накидання його на покажчик типу, який ми хочемо переосмислити як, а потім отримати доступ до значення, або іншими словами шляхом псевдоніму. Наприклад:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Як ми бачили раніше, це неправдивий псевдонім, тому ми посилаємось на невизначену поведінку. Але традиційно компілятори не скористалися суворими правилами дозволу, і цей тип коду, як правило, працював, розробники, на жаль, звикли робити такі дії. Поширений альтернативний метод для покарання типів - це об'єднання, що є дійсним в C, але невизначена поведінка в C ++ ( див. Прямий приклад ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
Це не є дійсним для C ++, і деякі вважають, що мета профспілок полягає лише у впровадженні варіантів типів, і вважають, що використання об'єднань для покарання типу є зловживанням.
Стандартний метод для типового покарання як для C, так і для C ++ - це memcpy . Це може здатися дещо важким, але оптимізатор повинен розпізнати використання memcpy для набору типів та оптимізувати його та створити регістр для реєстрації переміщення. Наприклад, якщо ми знаємо, що int64_t має той самий розмір, що і подвійний :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
ми можемо використовувати memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
При достатньому рівні оптимізації будь-який гідний сучасний компілятор генерує ідентичний код попередньо згаданому методу reinterpret_cast або об'єднанню для типового нанесення . Розглядаючи згенерований код, ми бачимо, що він використовує просто зареєструвати mov ( живий приклад компілятора Explorer ).
У C ++ 20 ми можемо отримати bit_cast ( реалізація доступна за посиланням із пропозиції ), що дає простий та безпечний спосіб набрати шрифт, а також бути корисним у контексті constexpr.
Нижче наводиться приклад того, як використовувати bit_cast для введення пункту неподписаного int для плавання , ( див. Це в прямому ефірі ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
У випадку, коли типи « До» та « Від» не мають однакового розміру, це вимагає від нас використання проміжної структури15. Ми будемо використовувати на структуру , яка містить SizeOf (беззнаковое ціле) масив символів ( передбачає 4 байта без знака Int ) , щоб бути від типу і без знака Int , що і щоб ввести.:
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
Прикро, що нам потрібен цей проміжний тип, але це обмеження в потоці bit_cast .
У нас не так багато хороших інструментів для лову суворого псевдоніму в C ++, інструменти, які ми маємо, виявлять деякі випадки суворих порушень збитку та деякі випадки нерівних навантажень і магазинів.
gcc, використовуючи прапор -fstrict-aliasing та -Wstrict-aliasing, може зафіксувати деякі випадки, хоча не без помилкових позитивів / негативів. Наприклад, наступні випадки генерують попередження в gcc ( дивіться його в прямому ефірі ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
хоча цей додатковий випадок не буде ( див. це наживо ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Хоча кланг дозволяє ці прапори, він, мабуть, насправді не реалізує попередження.
Ще один доступний нам інструмент - ASan, який може ловити нерівні вантажі та магазини. Незважаючи на те, що вони не є прямими чіткими порушеннями спостереження, вони є загальним результатом суворих порушень. Наприклад, наступні випадки створюватимуть помилки виконання під час побудови з клангом за допомогою -fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
Останній інструмент, який я порекомендую, - це специфічний для C ++ і не суворо інструмент, а практика кодування, не допускати кастингу в стилі C. І gcc, і clang будуть виробляти діагностику для анкет у стилі C, використовуючи -Wold-style-cast . Це змусить будь-які невизначені типи каламбурів використовувати reinterpret_cast, загалом reinterpret_cast повинен бути прапором для більш детального перегляду коду. Так само простіше здійснити пошук у вашій кодовій базі для reinterpret_cast для проведення аудиту.
Для C у нас усі інструменти вже охоплені, а також у нас є tis-interpreter, статичний аналізатор, який вичерпно аналізує програму для великого підмножини мови С. З огляду на версії C попереднього прикладу, коли використання -fstrict-aliasing пропускає один випадок ( див. Його в прямому ефірі )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-інтерпетер здатний спіймати всіх трьох, наступний приклад запускає tis-kernal як tis-interpreter (вихід редагується для стислості):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Нарешті, є TySan, який наразі розробляється. Цей дезінфікуючий додає інформацію про перевірку типів у сегменті тіньової пам'яті та перевіряє доступ, щоб перевірити, чи порушують вони правила дозволу. Інструмент потенційно повинен бути в змозі виявити всі спокійні порушення, але може мати великі накладні витрати.
reinterpret_cast
може робити або що cout
може означати. (Добре згадати C ++, але оригінальне запитання стосувалося C та IIUC, ці приклади можна було б так само достовірно записати в C.)
Строгий псевдонім не стосується лише покажчиків, він також впливає на посилання, я написав документ про нього для вікі-розробника, і він був настільки добре прийнятий, що перетворив його на сторінку свого консалтингового веб-сайту. Це повністю пояснює, що це таке, чому це так бентежить людей і що з цим робити. Сувора чиста книга . Зокрема, це пояснює, чому спілки є ризикованою поведінкою для C ++, і чому використання memcpy - це єдиний переносний виправлення як на C, так і на C ++. Сподіваюся, це корисно.
Як додаток до того, що Дуг Т. вже писав, ось простий тестовий випадок, який, ймовірно, викликає це з gcc:
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Компілювати з gcc -O2 -o check check.c
. Зазвичай (у більшості версій, які я пробував), це виводить "сувору проблему згладжування", оскільки компілятор припускає, що "h" не може бути такою ж адресою, як "k" у функції "check". Через це компілятор оптимізує if (*h == 5)
подальше і завжди викликає printf.
Для тих, кого тут цікавить, є код асемблера x64, створений gcc 4.6.3, який працює на ubuntu 12.04.2 для x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Тож умова if повністю відійшов від асемблерського коду.
long long*
та int64_t
*). Можна очікувати, що розумний компілятор повинен визнати, що a long long*
і int64_t*
може отримати доступ до одного і того ж сховища, якщо вони зберігаються однаково, але таке лікування вже не модне.
Введення покарань за допомогою покажчиків покажчиків (на відміну від використання об'єднання) є головним прикладом порушення строгого згладжування.
fpsync()
директиву між записом як fp та читанням як int або навпаки [щодо реалізацій з окремими цілими і FPU конвеєрами та кешами , така директива може бути дорогою, але не такою дорогою, як змушення компілятора виконувати таку синхронізацію при кожному доступі до об'єднання]. Або впровадження могло б вказати, що отримане значення ніколи не буде корисним, за винятком обставин, що використовують загальні початкові послідовності.
Відповідно до обґрунтування C89, автори Стандарту не хотіли вимагати, щоб компілятори надавали код типу:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
Потрібно вимагати, щоб перезавантажити значення x
між оператором присвоєння та поверненням таким чином, щоб забезпечити можливість, на яку p
може вказуватись x
, і присвоєння *p
може, відповідно, змінити значення x
. Думка про те, що компілятор повинен мати право припускати, що в таких ситуаціях, як зазначено вище, не буде згладжуватися, було суперечливим.
На жаль, автори C89 написали своє правило таким чином, що, якщо читати буквально, змусили б навіть таку функцію викликати Невизначене поведінку:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
тому що він використовує значення "lvalue" int
для доступу до об'єктів типу struct S
, і int
не входить до типів, які можуть використовуватися для доступу до struct S
. Оскільки було б абсурдно ставитись до будь-якого використання членів структур та об'єднань, що не є типовими символами, як Невизначена поведінка, майже кожен визнає, що існують принаймні деякі обставини, коли для доступу до об'єктів іншого типу може бути використане значення двох типів. . На жаль, Комітет зі стандартів С не зміг визначити, що це за обставини.
Значна частина проблем - це результат звіту про дефекти № 028, який запитав про поведінку програми на зразок:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
У звіті про дефекти №28 зазначено, що програма посилається на не визначену поведінку, оскільки дія написання члена профспілки типу "подвійний" та читання одного з типів "int" викликає поведінку, визначену реалізацією. Таке міркування є безглуздим, але є основою для правил Ефективного типу, які непотрібно ускладнюють мову, не роблячи нічого для вирішення початкової проблеми.
Найкращий спосіб вирішити первісну проблему, мабуть, буде поводити виноску про мету правила як би нормотворну та робить це правило неприйнятним, за винятком випадків, що насправді пов'язані з конфліктуючим доступом із використанням псевдонімів. Дано щось на кшталт:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Немає конфлікту всередині, inc_int
оскільки всі доступ до пам’яті, до якого здійснюється доступ *p
, робляться з типом lvalue int
, і немає конфлікту в test
тому, що p
це видимо походить від a struct S
, і при наступному s
використанні, всі доступ до цього сховища, який коли-небудь буде зроблений наскрізь p
вже відбулося.
Якщо код було трохи змінено ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Тут між конфліктом p
і доступом до s.x
цього позначеного рядка виникає, тому що в цьому місці виконання існує ще одне посилання, яке буде використовуватися для доступу до тієї ж пам’яті .
Якби звіт про дефекти 028 заявив, що оригінальний приклад посилався на UB через перекриття між створенням та використанням двох покажчиків, це зробило б речі більш зрозумілими без необхідності додавати "ефективні типи" чи іншу таку складність.
Прочитавши багато відповідей, я відчуваю потребу щось додати:
Суворий псевдонім (який я трохи опишу) важливий, оскільки :
Доступ до пам’яті може бути дорогим (функціональним), тому дані маніпулюються в регістрах процесора, перш ніж вони записуються у фізичну пам'ять.
Якщо дані в двох різних процесорах регістрів будуть записуватися в один і той же простір пам'яті, ми не можемо передбачити, які дані "виживуть", коли ми будемо кодувати в C.
У зборі, де ми кодуємо завантаження та вивантаження регістрів процесорів вручну, ми будемо знати, які дані залишаються недоторканими. Але С (на щастя) абстрагує цю деталь далеко.
Оскільки два покажчики можуть вказувати на одне місце в пам'яті, це може призвести до складного коду, який обробляє можливі зіткнення .
Цей додатковий код повільний і шкодить продуктивності, оскільки він виконує додаткові операції з читання / запису пам'яті, які є повільнішими та (можливо) непотрібними.
Суворе правило згладжування дозволяє уникнути надлишкового коду машини в тих випадках , в яких він повинен бути з упевненістю припустити , що два покажчика не вказують на той же блок пам'яті (дивись також restrict
ключове слово).
У строгому згладжуванні можна стверджувати, що вказівники на різні типи вказують на різні місця в пам'яті.
Якщо компілятор помітить, що два вказівники вказують на різні типи (наприклад, a int *
і a float *
), він припустить, що адреса пам'яті відрізняється, і вона не захистить від зіткнень адреси пам'яті, що призведе до більш швидкого машинного коду.
Наприклад :
Дозволяє виконувати наступну функцію:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Для того, щоб обробляти випадок, коли a == b
(обидва вказівки вказують на одну і ту ж пам’ять), нам потрібно замовити і протестувати спосіб завантаження даних з пам'яті в регістри процесора, щоб код міг закінчитися таким чином:
завантаження a
і b
з пам'яті.
додати a
до b
.
зберегти b
та перезавантажити a
.
(зберегти з реєстру процесора в пам'ять і завантажити з пам'яті в регістр процесора).
додати b
до a
.
зберегти a
(з реєстру процесора) в пам'ять.
Крок 3 дуже повільний, оскільки йому потрібно отримати доступ до фізичної пам'яті. Однак потрібно захищати від випадків, коли a
і b
вказують на ту саму адресу пам'яті.
Суворе згладжування дозволило б нам не допустити цього, сказавши компілятору, що ці адреси пам'яті відрізняються між собою (що в цьому випадку дозволить ще більше оптимізувати, що неможливо виконати, якщо покажчики поділяють адресу пам'яті).
Це можна сказати компілятору двома способами, використовуючи різні типи для вказівки. тобто:
void merge_two_numbers(int *a, long *b) {...}
Використання restrict
ключового слова. тобто:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Тепер, виконуючи правило суворого згладжування, кроку 3 можна уникнути, і код запуститься значно швидше.
Насправді, додавши restrict
ключове слово, всю функцію можна оптимізувати до:
завантаження a
і b
з пам'яті.
додати a
до b
.
зберегти результат і до, a
і до b
.
Цю оптимізацію раніше не можна було робити через можливе зіткнення (де a
і b
втричі замість подвоєння).
b
(не перезавантажуючи їх) та перезавантажуємо a
. Я сподіваюся, що зараз зрозуміліше.
restrict
, але я думаю, що останній у більшості обставин буде більш ефективним, а послаблення деяких обмежень register
дозволить йому заповнити деякі випадки, коли restrict
це не допоможе. Я не впевнений, що коли-небудь "важливо" ставитися до Стандарту як до повного опису всіх випадків, коли програмісти повинні очікувати, що компілятори визнають докази спотворення, а не просто описують місця, де компілятори повинні припускати псевдонім навіть тоді, коли конкретних доказів цього не існує .
restrict
ключове слово мінімізує не тільки швидкість операцій, але і їх кількість, що може мати значення ... Я маю на увазі, зрештою, найшвидша операція - це взагалі не операція :)
Строгий псевдонім не дозволяє різним типам вказівників на одні і ті ж дані.
Ця стаття повинна допомогти вам зрозуміти проблему повністю.
int
структура, яка містить an int
).
Технічно в C ++ суворе правило суворого псевдонімування, мабуть, ніколи не застосовується.
Зверніть увагу на визначення непрямості ( * оператор ):
Оператор Unary * виконує непряме: вираз, до якого він застосовується, має бути вказівником на тип об'єкта або вказівником на тип функції, а результат - значенням, що посилається на об'єкт або функцію, на яку вказує вираз .
Також з визначення glvalue
Glvalue - це вираз, оцінка якого визначає особистість об'єкта, (... snip)
Отже, у будь-якому чітко визначеному програмному сліді glvalue посилається на об'єкт. Тож так зване правило суворого псевдоніму не застосовується ніколи. Це може бути не те, чого хотіли дизайнери.
int foo;
, що має доступ до виразу lvalue *(char*)&foo
? Це об’єкт типу char
? Чи існує цей об’єкт одночасно foo
? Чи писали б foo
змінити збережене значення цього згаданого об'єкта типу char
? Якщо так, чи є якесь правило, яке дозволило б отримати доступ до збереженого значення об'єкта типу char
за допомогою значення lvalue int
?
int i;
створює декларація чотири об'єкти кожного типу символів in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` та i
. Нарешті, у Стандарті немає нічого, що дозволяє навіть volatile
кваліфікованому вказівнику отримувати доступ до апаратних регістрів, які не відповідають визначенню "об'єкт".
c
іc++faq
.