Каламбур типізації

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Каламбур типізації (англ. type punning) — термін в інформатиці для позначення різних технік порушення або «обману» системи типів деякої мови програмування, які мають ефект, який було б складно або неможливо забезпечити в рамках формальної мови.

Мови C і C++ надають явні можливості каламбуру типізації за допомогою таких конструкцій, як зведення типів, union, а також reinterpret_cast для C++, хоча стандарти цих мов деякі випадки таких каламбурів трактують як невизначену поведінку.

У мові Pascal записи з варіантами дозволяють інтерпретувати конкретний тип даних більш, ніж в один спосіб, або навіть у спосіб, не передбачений мовою.

Каламбур типізації є прямим порушенням типобезпеки. Традиційно можливість побудувати каламбур типізації пов'язують зі слабкою типізацією, але й деякі сильно типізовані мови або їх реалізації надають такі можливості (як правило, використовуючи у пов'язаних з ними ідентифікаторах слова unsafe або unchecked). Прихильники типобезпеки стверджують, що «необхідність» каламбурів типізації є міфом[1].

Приклади[ред. | ред. код]

Рядки та числа в JavaScript[ред. | ред. код]

JS дозволяє неявне зведення типів між рядками та числами, що може призводити до нелогічних результатів, наприклад:

console.log(2 + 2) // 4
console.log("2" + "2") // "22"
console.log(2 + 2 - 2) // 2
console.log("2" + "2" - "2") // "20"

Оператор + для чисел працює як додавання, а для рядків як конкатенація, проте оператор - працює тільки як віднімання для чисел, тому в останньому виразі ми отримуємо "22" - "2", що призводить до значення 20 .

Порівняння в JavaScript[ред. | ред. код]

Порівняння між значеннями різних типів JS не транзитивне:

0 == "0"
0 == []
"0" != []

Сокети в C[ред. | ред. код]

Класичний приклад каламбуру типізації можна побачити в інтерфейсі сокетів Берклі. Функція, яка пов'язує відкритий неініціалізований сокет з IP-адресою, має таку сигнатуру:

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

Функцію bind зазвичай викликають так:

struct sockaddr_in sa = {0};
int sockfd = ...;
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
bind(sockfd, (struct sockaddr *)&sa, sizeof sa);

Бібліотека сокетів Берклі у своїй основі спирається на той факт, що в мові C вказівник на struct sockaddr_in може безперешкодно перетворюватися на вказівник на struct sockaddr, а також що обидва структурні типи частково збігаються щодо організації подання в пам'яті. Отже, вказівник на поле my_addr->sin_family (де my_addr має тип struct sockaddr*) насправді вказуватиме на поле sa.sin_family (де sa має тип struct sockaddr_in). Іншими словами, бібліотека використовує каламбур типізації для реалізації примітивної форми наслідування[2].

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

Числа з рухомою комою[ред. | ред. код]

Припустимо, потрібно перевірити, що число з рухомою комою є від'ємним. Можна було б написати:

bool is_negative(float x) {
  return x < 0.0;
}

Однак, порівняння чисел із рухомою комою є ресурсомісткими, оскільки діє в особливий спосіб для NaN. Узявши до уваги, що тип float подано згідно стандарту IEEE 754-2008, а тип int має розмір 32 біти і за знак у ньому відповідає той самий біт, що й у float, можна для отримання знакового біту числа з рухомою комою застосувати каламбур типізації, використавши тільки цілочисельне порівняння:

bool is_negative(float x) {
  return *((int*)&x) < 0;
}

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

Реальний приклад можна знайти в коді Quake III — див. Швидкий обернений квадратний корінь.

На додаток до припущень про бітове подання чисел з рухомою комою наведений вище приклад каламбуру типізації також порушує встановлені мовою C правила доступу до об'єктів[3]: x оголошено як float, але його значення зчитується у виразі, що має тип signed int. На багатьох поширених платформах такий каламбур типізації вказівників може призвести до проблем, якщо вказівники по-різному вирівняно в пам'яті. Більш того, вказівники різного розміру можуть здійснювати спільний доступ до певних дільнок пам'яті, спричиняючи помилки, яких не може виявити компілятор.

Використання union[ред. | ред. код]

Проблему суміщення назв можна вирішити за допомогою union (хоча приклад нижче ґрунтується на припущенні, що число з рухомою комою подано за стандартом IEEE-754):

bool is_negative(float x) {
  union {
    unsigned int ui;
    float d;
  } my_union = { .d = x };
  return (my_union.ui & 0x80000000) != 0;
}

Це код на C99 з використанням позначених ініціалізаторів (англ. Designated initialisers). При створенні об'єднання ініціалізується його дійсне поле, а потім відбувається читання значення цілого поля (фізично розміщеного в пам'яті на тій самій адресі), згідно з пунктом s6.5 стандарту. Деякі компілятори підтримують такі конструкції як розширення мови, наприклад, GCC[4].

Як ще один приклад каламбуру типізації див. Крок масиву[en].

Паскаль[ред. | ред. код]

Варіантний запис дозволяє розглядати тип даних по-різному, залежно від зазначеного варіанту. У цьому прикладі передбачається, що integer має розмір 16 біт, longint і real — 32 біти, а character — 8 біт:

 type variant_record = record
   case rec_type : longint of
     1: ( I : array [1..2] of integer );
     2: ( L : longint );
     3: ( R : real );
     4: ( C : array [1..4] of character );
   end;
  Var V: Variant_record;
   K: Integer;
   LA: Longint;
   RA: Real;
   Ch: character;
 ...
  V.I := 1;
  Ch := V.C[1];  (* Отримуємо перший байт поля V.I *)
  V.R := 8.3;
  LA := V.L;   (* Зберігаємо дійсне число в цілочисельну комірку *)

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

 Type PA = ^Arec;

  Arec = record
   case rt : longint of
     1: (P: PA);
     2: (L: Longint);
  end;

 Var PP: PA;
  K: Longint;
 ...
  New(PP);
  PP^.P := PP;
  Writeln('Змінна PP міститься в пам''яті за адресою ', hex(PP^.L));

Стандартна процедура New в Паскалі призначена для динамічного виділення пам'яті для вказівника, а під hex мається на увазі певна процедура, що друкує шістнадцяткове подання цілого числа. Це дозволяє вивести на екран адресу вказівника, що зазвичай заборонено (вказівники в Паскалі можна лише присвоювати, але не читати чи виводити). Присвоєння значення цілому варіанту вказівника дозволяє читати та змінювати будь-яку ділянку системної пам'яті:

 PP^.L := 0;
 PP := PP^.P; (* PP вказує на адресу 0 *)
 K := PP^.L;  (* K містить значення слова за адресою 0 *)
 Writeln(' Слово за адресою 0 цієї машини містить ', K);

Ця програма може працювати коректно або впасти, якщо адресу 0 захищено від читання, залежно від операційної системи.

Див. також[ред. | ред. код]

Примітки[ред. | ред. код]

  1. Lawrence C. Paulson. ML for the Working Programmer. — 2nd. — Cambridge, Great Britain : Cambridge University Press, 1996. — С. 2. — ISBN 0-521-57050-6 (тверда обкладинка), 0-521-56543-X (м'яка обкладинка).
  2. struct sockaddr_in, struct in_addr. www.gta.ufrj.br. Архів оригіналу за 24 січня 2016. Процитовано 17 січня 2016.
  3. ISO/IEC 9899:1999 s6.5/7
  4. GCC: Non-Bugs. Архів оригіналу за 22 листопада 2014. Процитовано 21 листопада 2014.

Посилання[ред. | ред. код]

Теорія
Мова C — звіти про дефекти стандарту C99
  • Розділ посібника з компілятора GCC щодо опції -fstrict-aliasing, що запобігає деяким каламбурам типізації
  • Defect Report 257, що випадково визначає «каламбур типізації» за допомогою union і обговорює поведінку наведеного вище коду, що залежить від реалізації .
  • Defect Report 283 про використання типу union для каламбурів типізації
Типобезпечні мови