Целочисленная арифметика вызывает странный результат (округление после деления?)

Использование gcc версии 4.8.4 для Linux, короткий 16 бит, а int 32 бит.

#include "stdio.h"
int main( void ){
  unsigned short u = 0xaabb;
  unsigned int   v = 0xaabb;
  printf ("%08x %08x\n", u, (unsigned short)((u*0x10001)/0x100));
  printf ("%08x %08x\n", v, (unsigned short)((v*0x10001)/0x100));
  return 0;
}

Результат:

0000aabb 0000bbab
0000aabb 0000bbaa

Это может быть изменено, например, путем деления на 0x10, что дает аналогичный результат (+1) для первого случая. Эффект не возникает, если байт, усеченный на /0x100, меньше 0x80. Машинный код для первого случая (short u) выглядит так, как будто предполагается округление (добавление 0xFF).

  • В чем причина результата или это ошибка?
  • Каков результат для других компиляторов?

Ответ 1

Литерал, подобный 0x10001, будет иметь тип int (если он может поместиться внутри int, что в этом случае истинно). int является подписанным типом.

Так как переменная u является малым целым типом, она получает целочисленное значение до int всякий раз, когда используется в выражении.

0xaabb * 0x10001 предположительно даст результат 0xAABBAABB. Однако этот результат слишком велик, чтобы помещаться внутри int в 32-битной системе с двумя дополнениями, где наибольшее число для int составляет 0x7FFFFFFF. Таким образом, вы получаете переполнение по целому знаку со знаком и, следовательно, вызываете поведение undefined - все может случиться.

Никогда не используйте знаковые целые числа при выполнении любой формы двоичной арифметики!

Кроме того, окончательный перевод в (unsigned short) бесполезен, потому что аргумент printf все равно передает переданное значение в int. Это тоже строго неверно, потому что %x означает, что printf ожидает unsigned int.

Чтобы избежать всех проблем с непредсказуемыми и ограниченными по умолчанию целыми типами в C, используйте stdint.h. Кроме того, использование неподписанных int-литералов решает множество неявных ошибок продвижения типов.

Пример:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main( void ){
  uint16_t u = 0xaabb;
  uint16_t v = 0xaabb;
  printf ("%08" PRIx16 " %08" PRIx16 "\n", u, (uint16_t)(u*0x10001u/0x100u));
  printf ("%08" PRIx16 " %08" PRIx16 "\n", v, (uint16_t)(v*0x10001u/0x100u));
  return 0;
}

(Этот код также будет содержать аргумент, но с помощью спецификатора формата PRIx16 вы сообщите printf, что теперь бизнес-компилятор делает работу с кодом, независимо от того, какие рекламные кампании типа могут присутствовать в вызов функции.)

Ответ 2

Обычные арифметические преобразования при игре.

u перед умножением преобразуется в int. Поскольку int подпишет, он ведет себя по-разному при делении.

printf("%08x\n", (u*0x10001)/0x100);
printf("%08x\n", (v*0x10001)/0x100);

Возвращает

ffaabbab
00aabbaa

Строго говоря, переполнение переполнения в значении integer уже имеет поведение undefined, поэтому результат недействителен еще до деления.

Ответ 3

Результатом u*0x10001 является int= вызывает переполнение типа signed и, следовательно, поведение undefined.

Ответ 4

Предположим, что 16 бит short и 32 бит int (типичный для x86, ARM и большинства других 32-разрядных систем):

В коде есть два типа поведения undefined (UB). Во-первых, вы используете неправильные спецификаторы типов в строках формата. %x ожидает unsigned int, а вы передаете unsigned short, расширенный до signed int.

Второй - и тот, который вы видите здесь, - это первый расчет: u преобразуется в int (целые акции) - не unsigned int для умножения, потому что константа 0x10001 также int. Умножение вызывает UB, когда он генерирует целочисленное переполнение целых чисел. Когда вы вызываете UB, вы теряетесь, и любая дальнейшая интерпретация бесполезна.

Сказано, что мы сейчас размышляем: происходит то, что после умножения вы, скорее всего, имеете отрицательное значение, а так как деление округляется к нулю (это стандартное требование), вы получаете более высокое отрицательное значение. Но по мере того как вы печатаете как unsigned, вы видите большее значение raw (unsigned). Это связано с внутренним представлением отрицательных значений из 2 дополнений.


Обратите внимание, что этот результат выходит за рамки стандарта C. На самом деле компилятор мог генерировать код для форматирования вашего жесткого диска, или компьютер мог выпрыгнуть из окна, или демоны носа могли появиться. Итак, исправьте ошибки:

  • используйте %hx для печати unsigned short int
  • например. используйте u * 0x10001U для принудительного преобразования в unsigned int для умножения. В общем случае рекомендуется всегда использовать суффикс u (без знака), если вы работаете с неподписанными значениями.

Ответ 5

Я немного расширил ваш код, чтобы объяснить:

#include "stdio.h"
int main( void ){
  unsigned short u = 0xaabb;
  unsigned int   v = 0xaabb;

  printf ("not casted:\n");
  printf ("%08x %08x\n", u, ((u*0x10001)/0x100));
  printf ("%08x %08x\n", v, ((v*0x10001)/0x100));

  printf ("unsigned short casted:\n");
  printf ("%08x %08x\n", u, (unsigned short)((u*0x10001)/0x100));
  printf ("%08x %08x\n", v, (unsigned short)((v*0x10001)/0x100));

  printf ("u*0x10001:\n");
  printf ("x=%08x d=%d\n", u*0x10001, u*0x10001);

  // Solution
  printf ("Solution:\n");
  printf (">>> %08x %08x\n", u, (unsigned short)((u*0x10001UL)/0x100UL));
  printf (">>> %08x %08x\n", v, (unsigned short)((v*0x10001UL)/0x100UL));
  return 0;
}

Это приводит к следующему выводу:

not casted:
0000aabb ffaabbab
0000aabb 00aabbaa
unsigned short casted:
0000aabb 0000bbab
0000aabb 0000bbaa
u*0x10001:
x=aabbaabb d=-1430541637
Solution:
>>> 0000aabb 0000bbaa
>>> 0000aabb 0000bbaa

Итак, что вы видите, что операция u*0x10001 будет генерировать значение signed int (32 бит), и из-за этого ваш результат d=-1430541637. Если вы разделите это значение на 0x100, вы получите результат, который вы получили 0xFFAABBAB. Если вы выбрали это значение с помощью unsigned short, как вы это сделали, вы получите свой результат = 0x0000BBAB. Если вы хотите предотвратить это, чтобы компилятор использовал неподписанные значения для этой операции, вам нужно написать UL в качестве расширения для чисел.

Итак, вы видите, что компилятор работает как ожидалось. Вы можете скомпилировать его самостоятельно Код [^].