From 2ed6ddd3c6c8308ee1f828875d30738f73bbc4e7 Mon Sep 17 00:00:00 2001 From: Nekrolm Date: Sun, 19 Feb 2023 20:31:37 +0000 Subject: [PATCH] sign extension --- README.md | 1 + numeric/char_sign_extension.md | 92 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 numeric/char_sign_extension.md diff --git a/README.md b/README.md index 4779885..443df68 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ 1. [Переполнение знаковых целых чисел](numeric/overflow.md) 2. [Числа с плавающей точкой](numeric/floats.md) 3. [Integer promotion](numeric/integer_promotion.md) + 4. [char и знаковое расширение](numeric/char_sign_extension.md) 4. Нарушения lifetime объектов 1. [Висячие ссылки — общие случаи](lifetime/use_after_free_in_general.md) 2. [Автовывод типов и висячие ссылки](lifetime/decltype_auto_and_explicit_types.md) diff --git a/numeric/char_sign_extension.md b/numeric/char_sign_extension.md new file mode 100644 index 0000000..94a6b18 --- /dev/null +++ b/numeric/char_sign_extension.md @@ -0,0 +1,92 @@ +# char и знаковое расширение + +Возьмем следующую простенькую структуру + +```C++ +// пример утащен и изменен отсюда: +// https://twitter.com/hankadusikova/status/1626960604412928002 +struct CharTable { + static_assert(CHAR_BIT == 8); + std::array _is_whitespace {}; + + CharTable() { + _is_whitespace.fill(false); + } + + bool is_whitespace(char c) const { + return this->_is_whitespace[c]; + } +}; +``` + +Все ли впорядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами. + + +Давайте [потестируем](https://godbolt.org/z/75rTW1nMG) + +```C++ +int main() { + CharTable table; + char c = 128; + bool is_whitespace = table.is_whitespace(c); + std::cout << is_whitespace << "\n"; + return is_whitespace; +} +``` + +При сборке с `-fsanitize=undefined` получаем дивный результат + +``` +/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36: runtime error: index 18446744073709551488 out of bounds for type 'bool [256]' +/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36: runtime error: index 18446744073709551488 out of bounds for type 'bool [256]' +/app/example.cpp:14:38: runtime error: load of value 64, which is not a valid value for type 'bool' +``` + +Конкретное значение в третьей строке -- совершенно случайное. Было бы очень здорово стабильно увидеть 42, но увы. + +Зато индекс в первых двух строках совсем не случайный. + +Но погодите! +`char c = 128;` это же точно меньше 256. Откуда `18446744073709551488`? + +Будем разбираться. В деле замешаны две удачно разложенные ловушки. + +1. С/C++ специфичная ловушка: знаковость типа `char` не специфицирована. В зависимости от платформы он может быть как знаковым, так и беззнаковым. На x86 чаще всего является знаковым. И из `char c = 128` получается `c = -128`. + +2. Ловушка, распространенная во многих языках, имеющих разные типы целых чисел, разной знаковости и длины. Например, [Rust](https://godbolt.org/z/cY1v3rvrK) +```Rust +pub fn main() { + let c : i8 = -5; + let c_direct_cast = c as u16; + let c_two_casts = c as u8 as u16; + println!("{c_direct_cast} != {c_two_casts}"); +} +``` +Мы увидим `65531 != 251`. + +При преобразовании знакового целого меньшей длины к беззнаковому целому большей длины происходит знаковое расширение: старшие биты заполняются битом знака. + +Тоже [действует и в C/C++](https://godbolt.org/z/cfcdb5fr3). + + +А теперь остается только взглянуть на сигнатуру `std::array::operator[]`: +```C++ +reference operator[]( size_type pos ); +``` + +`size_type` это беззнаковый `size_t`. Под x86 он определенно больше чем `char`. +Происходит прямой каст знакового `char` в `size_t`, знак расширяется, код ломается. Дело закрыто. + +# Что делать + +Со знаковым расширением иногда способны помочь статические анализаторы. +Нужно понимать что вы делаете при касте чисел и что хотите получить. Часто можно встретить конструкцию вида `uint32_t extended_val = static_cast(byte_val) & 0xFF`, чтоб гарантированно занулить верхние байты и избежать знакового расширения. Аналогичная конструкция может быть и при преобразовании `int32 -> uint64`, и при любых других комбинациях -- только константу правильную писать не забывайте. + +Из-за своей знаковой неспецифицированности тип `char` очень опасен при работе с ним как с типом чисел. Крайне рекомендуется пользоваться соответствующими типами `uint8_t` или `int8_t`. Или другими подходящими, если на вашей целевой платформе в `char` внезапно не 8 бит. + + +# Полезные ссылки +1. https://en.cppreference.com/w/cpp/language/types +2. https://en.cppreference.com/w/cpp/container/array/operator_at +3. https://en.cppreference.com/w/cpp/types/climits +4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html \ No newline at end of file