Как c компилятор обрабатывает unsigned и signed integer? Почему код сборки для беззнаковой и подписанной арифметической операции одинаковый?

Я читаю книгу: CS-APPe2. C имеет неподписанный и подписанный тип int, и в большинстве архитектур использует двоичную арифметику для реализации знакового значения; но, изучив некоторый код сборки, я обнаружил, что очень мало инструкций различают неподписанные и подписанные. Поэтому мой вопрос:

  • Обязан ли компилятор дифференцировать подписанные и без знака? Если да, то как это делается?

  • Кто реализует арифметику с двумя дополнениями - процессор или компилятор?

Добавьте дополнительную информацию:

Изучив еще несколько инструкций, на самом деле есть некоторые из них, которые различают подписанные и unsigned, такие как setg, seta и т.д. Кроме того, CF и OF применяются к неподписанным и, соответственно. Но большинство целочисленных арифметических команд обрабатывают unsigned и подписывают то же самое, например.

int s = a + b

и

unsigned s = a + b

генерирует ту же инструкцию.

Итак, при выполнении ADD s d, если процессор обрабатывает s & d без знака или подписан? Или это не имеет значения, потому что бит-шаблон обоих результатов одинаковый, и задача компилятора преобразует результат базового битового шаблона в беззнаковый или подписанный?

P.S Я использую x86 и gcc

Ответ 1

Это довольно легко. Операции, такие как сложение и вычитание, не нуждаются в корректировке для подписанных типов в двух арифметических элементах. Просто выполните эксперимент разума и представьте себе алгоритм, используя только следующие математические операции:

  • приращение на один
  • уменьшение на один
  • сравнить с нулем

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

Если вы хотите увидеть какую-либо разницу, я рекомендую вам попробовать операции, которые небезопасны в отношении переполнения. Одним из примеров является сравнение (a < b).

Являетесь ли вы ответственным за дифференциацию подписанных и без знака? Если да, то как это сделать?

При необходимости генерируя различные сборки.

Кто реализует арифметику с двумя дополнениями - процессор или компилятор?

Это сложный вопрос. Два дополнения, вероятно, являются наиболее естественным способом работы с отрицательными целыми числами на компьютере. Большинство операций для двух дополнений с переполнением такие же, как для целых без знака с переполнением. Знак можно извлечь из одного бита. Сравнение может быть концептуально выполнено путем вычитания (которое является агностиком подписи), изъятием знакового бита и сравнением с нолем.

Это арифметические функции процессора, которые позволяют компилятору производить вычисления в двух дополнениях.

unsigned s = a + b

Обратите внимание, что путь плюс вычисляется здесь, не зависит от типа результата. Insead зависит от типов переменных справа от знака равенства.

Итак, при выполнении ADD s d, если процессор обрабатывает s & d без знака или подписан?

Инструкции CPU не знают о типах, которые используются компилятором. Кроме того, нет никакой разницы между добавлением двух чисел без знака и добавлением двух подписанных чисел. Было бы глупо иметь две инструкции для одной и той же операции.

Ответ 2

Во многих случаях на уровне машины нет разницы между подписанными и неподписанными операциями, и это просто вопрос интерпретации битового шаблона. Например, рассмотрим следующую операцию с 4-мя словами:

Binary Add  Unsigned   2 comp
----------  --------   --------
  0011          3         3
+ 1011       + 11       - 5
-------     --------   --------
  1110         14        -2  
-------     --------   --------

Двоичный шаблон одинаковый для подписанной и неподписанной операции. Обратите внимание, что вычитание является просто добавлением отрицательного значения. Когда выполняется SUB-операция, правый операнд состоит из двух дополняемых (инвертирует биты и приращение), а затем добавляется (ответная цепь ALU является сумматором); не на уровне инструкции, который вы понимаете, но на логическом уровне, хотя можно было бы реализовать машину без инструкции SUB и все же выполнять вычитание, хотя и в двух инструкциях, а не в одном.

Существуют некоторые операции, которые требуют разных инструкций в зависимости от типа, и ответственность компилятора заключается в том, чтобы генерировать соответствующий код, вообще говоря, могут применяться архитектурные варианты.

Ответ 3

Нет необходимости различать подписи и unsigned int для большинства арифметических/логических операций. Часто приходится учитывать знак при печати, ноль/знак, расширяющий или сравнивающий значения. На самом деле CPU ничего не знает о типе значения. 4-байтовое значение представляет собой всего лишь ряд бит, оно не имеет никакого значения, если пользователь не указывает, что float, массив из 4 символов, unsigned int или подписанный int и т.д. Например, при печати char, в зависимости от указанных свойств типа и вывода, он распечатает символ, целое число без знака или целое число со знаком. Ответственность программиста заключается в том, чтобы показать компилятору, как обрабатывать это значение, а затем компилятор выдает правильную инструкцию, необходимую для обработки значения.

Ответ 4

Это беспокоило меня слишком долго. Я не знал, как компилятор работает как программа, передавая свои умолчания и неявные инструкции. Но мой поиск ответа привел меня к следующим выводам:

Реальный мир использует только целые числа со знаком, так как открытие отрицательных чисел. то есть причина, по которой int считается стандартным знаком в компиляторе. Я полностью игнорирую арифметику без знака, поскольку она бесполезна.

ЦП не имеет понятия целых чисел без знака. Он просто знает бит - 0 и 1. Как вы интерпретируете его вывод, зависит от вас как программист сборки. Это делает программирование сборки утомительным. Работа с целыми числами (подписанная и неподписанная) включала много проверок флагов. Именно поэтому были разработаны языки высокого уровня. компилятор отнимает всю боль.

Как работает компилятор, это очень продвинутое обучение. Я признал, что в настоящее время это выходит за рамки моего понимания. Это признание помогло мне двигаться дальше.

В архитектуре x86:

add и вспомогательные инструкции изменяют флаги в регистре eflags. Эти флаги могут использоваться вместе с инструкциями adc и sbb для построения арифметики с большей точностью. В этом случае мы переместим размер чисел в регистр ecx. Количество команд цикла цикла выполняется так же, как размер чисел в байтах.

Sub-инструкция берет 2 дополнения к вычитанию, добавляет их в minuend, инвертирует перенос. Это делается в аппаратном обеспечении (реализовано в схеме). Sub-команда "активирует" другую схему. После использования вспомогательной инструкции программист или компилятор проверяет CF. Если оно равно 0, результат будет положительным, а получатель имеет правильный результат. Если оно равно 1, результат отрицательный, а пункт назначения имеет 2 дополнения к результату. Обычно результат остается в 2 дополнениях и считывается как подписанный номер, но инструкции NOT и INC могут использоваться для его изменения. Команда NOT выполняет 1 дополнение к операнду, затем операнд увеличивается, чтобы получить 2 дополнения.

Когда программист планирует прочитать результат добавления или вспомогательной инструкции как подписанный номер, он должен следить за флагом OF. Если он установлен 1, результат будет неправильным. Он должен подписывать - расширять числа перед запуском операции между ними.

Ответ 5

Было сказано много о вашем первом вопросе, но мне нравится что-то сказать о вашем втором:

Кто реализует арифметику с двумя дополнениями - процессор или компилятор?

Стандарт C не требует, чтобы отрицательные числа имели два дополнения, он вообще не определяет, как аппаратные средства выражают отрицательные числа. Задача компилятора заключается в том, чтобы перевести код C в инструкции ЦП, которые выполняют то, что вы запрашиваете у своего кода. Таким образом, независимо от того, будет ли компилятор C создавать код для арифметики с двумя дополнениями или нет, это зависит только от того, использует ли ваш процессор арифметику с двумя дополнениями или нет. Компилятор должен знать, как работает ЦП и создает код соответственно. Поэтому правильный ответ на этот вопрос: CPU.

Если ваш процессор использовал представление с одним дополнением, чем компилятор C для этого ЦП, он выдавал инструкции с одним дополнением. С другой стороны, компилятор C может эмулировать поддержку отрицательных чисел на процессоре, который вообще не знает отрицательных чисел. Поскольку двухкомпонентное дополнение позволяет вам игнорировать, если число подписано или не указано для многих операций, это не так сложно сделать. В этом случае это был бы компилятор, реализующий арифметику с двумя дополнениями. Это также может быть сделано на процессоре, который имеет представление для отрицательных чисел, но почему компилятор должен это сделать, а не просто использовать встроенную форму, которую понимает процессор? Поэтому он не будет этого делать, если это не нужно.

Ответ 6

2 - это просто карта между десятичным и двоичным числом.

Компилятор реализует это сопоставление путем перевода литерального числа в соответствующий двоичный код, например -3 в 0xFFFFFFFD (как это видно при разборке), и создания машинного кода, который согласуется с представлением с двумя дополнениями. Например, когда он пытается выполнить 0 -3, он должен выбрать instuction, который должен произвести 0xFFFFFFFD, приняв 0x00000000 и 0x000000003 в качестве аргументов.

Причина, по которой он выбирает SUB, который является таким же для вычитания без знака, просто выдает 0xFFFFFFFD, как ожидалось. Нет необходимости запрашивать у ЦП специальный SUB для вычитания с надписью. Сказать, что второй операнд инвертируется 2-мя дополнениями и этим выводом вывод о том, что ЦП реализует 2 дополнения, является несправедливым. Поскольку заимствование из более высокого бита в вычитание происходит так же, как 2-обратное преобразование, и когда SUB используется для вычитания без знака, нет необходимости вообще включать концепцию 2 дополнения.

Следующая разборка иллюстрирует тот факт, что вычитание с подписью использует тот же SUB, что и без знака.

//int32_3 = -3;
010B2365  mov         dword ptr [int32_3],0FFFFFFFDh  
//int32_1 = 0, int32_2 = 3;
010B236C  mov         dword ptr [int32_1],0  
010B2373  mov         dword ptr [int32_2],3  
//uint32_1 = 0, uint32_2 = 3;
010B237A  mov         dword ptr [uint32_1],0  
010B2384  mov         dword ptr [uint32_2],3  
//int32_3 = int32_1 - int32_2;
010B238E  mov         eax,dword ptr [int32_1]  
010B2391  sub         eax,dword ptr [int32_2]  
010B2394  mov         dword ptr [int32_3],eax  
//uint32_3 = uint32_1 - uint32_2;
010B2397  mov         eax,dword ptr [uint32_1]  
010B239D  sub         eax,dword ptr [uint32_2]  
010B23A3  mov         dword ptr [uint32_3],eax  

ЦП сохраняет дополнительную информацию в флагах CF и OF для дальнейших инструкций, которые используют результат SUB по-разному в зависимости от типа переменной, которому присваивается результат.

Следующая демонстрация иллюстрирует, как компилятор генерирует разные инструкции для сопоставленного сравнения и беззнакового сравнения. Обратите внимание, что cmp включает внутренний sub, а jle основан на jbe OF, а jbe - на флагом CF.

//if (int32_3  > 1)  int32_3 = 0;
010B23A9  cmp         dword ptr [int32_3],1  
010B23AD  jle         main+76h (010B23B6h)  
010B23AF  mov         dword ptr [int32_3],0  
//if (uint32_3 > 1) uint32_3 = 0;
010B23B6  cmp         dword ptr [uint32_3],1  
010B23BD  jbe         main+89h (010B23C9h)  
010B23BF  mov         dword ptr [uint32_3],0 

Именно OF отдает тот факт, что CPU реализует 2 дополнения, потому что путь OF установлен, когда средний двоичный номер 0x10000000 или 0x0FFFFFFF превышен. И 2 дополнения представления отображает 0x10000000 на -268435456 и 0x0FFFFFFF на 268435455, которые являются верхним и нижним пределом 32-битного целого числа. Таким образом, этот флаг OF разработан специально для 2-х дополнений, поскольку другое представление может отображать другие двоичные числа в верхнем и нижнем пределе.

В заключение: 1. Компилятор отличает арифметику с подписью и без знака, реализуя соответствующие представления (сопоставление) и генерируя инструкции, результат которых соответствует представлению компилятора целого числа без знака. 2. Компилятор реализует 2 представления дополнений, а ЦП также реализует его для поддержки компилятора в генерации арифметических инструкций, результат которых соответствует представлению 2-го дополнения.