Interested Article - Каламбур типизации

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

Языки Си и C++ предоставляют явные возможности каламбура типизации посредством таких конструкций, как приведение типов , union , а также reinterpret_cast для C++ , хотя стандарты этих языков некоторые случаи таких каламбуров трактуют, как неопределённое поведение .

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

Каламбур типизации является прямым нарушением типобезопасности . Традиционно возможность построить каламбур типизации связывается со слабой типизацией , но и некоторые сильно типизированные языки или их реализации предоставляют такие возможности (как правило, используя в связанных с ними идентификаторах слова unsafe или unchecked ). Сторонники типобезопасности утверждают, что « необходимость » каламбуров типизации является мифом .


Примеры

Строки и числа в 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" != []

Сокеты в Си

Классический пример каламбура типизации можно видеть в интерфейсе сокетов Беркли . Функция , которая связывает открытый неинициализированный сокет с 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);

Библиотека сокетов Беркли в своей основе опирается на тот факт, что в языке Си указатель на struct sockaddr_in может беспрепятственно преобразовываться в указатель на struct sockaddr , а также что оба структурных типа частично совпадают по организации представления в памяти . Следовательно, указатель на поле my_addr->sin_family (где my_addr имеет тип struct sockaddr* ) на самом деле будет указывать на поле sa.sin_family (где sa имеет тип struct sockaddr_in ). Другими словами, библиотека использует каламбур типизации для реализации примитивной формы наследования .

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

Числа с плавающей запятой

Предположим, требуется проверить, что число с плавающей запятой является отрицательным. Можно было бы написать:

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;
}

Такая форма каламбура типизации является наиболее опасной. Предыдущий пример опирался только на гарантии, данные языком Си в отношении представления структур и преобразуемости указателей ; однако, данный пример опирается на предположения в отношении конкретного аппаратного обеспечения . В некоторых случаях, например, при разработке приложений реального времени , которые компилятор не способен оптимизировать самостоятельно, такие опасные программные решения оказываются необходимыми. В таких случаях обеспечить поддерживаемость кода помогают комментарии и проверки времени компиляции ( англ. ).

Реальный пример можно найти в коде Quake III — см. Быстрый обратный квадратный корень .

В дополнение к предположениям о битовом представлении чисел с плавающей запятой вышеприведённый пример каламбура типизации также нарушает установленные языком Си правила доступа к объектам : 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 .

В качестве ещё одного примера каламбура типизации см. (англ.) .

Паскаль

Вариантная запись позволяет рассматривать тип данных различным образом в зависимости от указанного варианта. В следующем примере предполагается, что 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. — 492 с. — ISBN 0-521-57050-6 (твёрдый переплёт), 0-521-56543-X (мягкий переплёт).
  2. . www.gta.ufrj.br. Дата обращения: 17 января 2016. 24 января 2016 года.
  3. ISO/IEC 9899:1999 s6.5/7
  4. . Дата обращения: 21 ноября 2014. 22 ноября 2014 года.

Ссылки

  • Теория:
  • Язык Си — отчёты о дефектах стандарта C99 :
    • по компилятору GCC касательно опции -fstrict-aliasing , предотвращающей некоторые каламбуры типизации
    • , случайно определяющий « каламбур типизации » посредством union и обсуждающий зависящее от реализации поведение вышеприведённого кода
    • об использовании типа union для каламбуров типизации
  • Типобезопасные языки:
Источник —

Same as Каламбур типизации