Знаковые и беззнаковые операции

Урок показывает, где знаковость реально влияет на команды: расширение, переходы и переполнение.

Знаковость — это не свойство битов, а решение программиста, как их трактовать; от него зависят некоторые команды.

Где машина всё-таки различает знак

Сложение и вычитание одинаковы для знаковых и беззнаковых — спасибо дополнительному коду. Но в трёх местах разница принципиальна: при расширении числа в больший регистр, при сравнении и переходах и при делении/умножении.

Расширение: movzx и movsx

Когда маленькое число кладут в большой регистр, надо решить, чем заполнить старшие биты:

movzx rax, al   ; zero-extend: старшие биты = 0  (беззнаково)
movsx rax, al   ; sign-extend: повторить знаковый бит (знаково)

Для байта 0xFF: как беззнаковый он 255, и movzx даст 255 в rax. Как знаковый он −1, и movsx заполнит rax единицами, сохранив −1.

Парные переходы

После cmp для одного и того же отношения есть два набора переходов:

СмыслБеззнаковыйЗнаковый
большеja (above)jg (greater)
меньшеjb (below)jl (less)

Выбор неверного набора — классический баг: для байта 0xFF «больше или меньше» нуля зависит от того, считаем мы его 255 или −1.

Как работает под капотом

Промоделируем расширение байта двумя способами на Python:

b = 0xFF   # один байт
# movzx: дополняем нулями
zx = b
# movsx: если старший бит 1, считаем знаковым -1
sx = b - 256 if b & 0x80 else b
print("movzx ->", zx)    # 255
print("movsx ->", sx)    # -1

Вывод:

movzx -> 255
movsx -> -1

Одни и те же входные биты дают разный результат в большом регистре — всё решает выбор команды расширения. Компилятор C делает этот выбор за вас, исходя из того, объявлена переменная как unsigned или нет.

Частые ошибки

  • Брать ja/jb для знаковых чисел. Отрицательное число с битом-знаком будет «больше» по беззнаковому сравнению — логика сломается.
  • Расширять movzx там, где число знаковое. Тогда −1 превратится в 255 — тихая ошибка.
  • Считать, что переполнение всегда видно. Беззнаковое отслеживает CF, знаковое — OF; смотреть надо нужный флаг.

Итог

  • Сложение/вычитание не зависят от знака, но расширение, сравнение и деление — зависят.
  • movzx дополняет нулями (беззнаково), movsx — знаковым битом (знаково).
  • Для сравнений есть парные переходы: ja/jb (беззнак.) и jg/jl (знак.).
  • Знаковость задаёт программист (или тип в C), биты сами по себе нейтральны.
Проверьте себя
1. Чем movzx отличается от movsx?
Amovzx дополняет нулями, movsx — знаковым битом
Bmovzx работает только с rax
Cmovsx быстрее
DОни идентичны для байтов
2. Какой переход нужен для сравнения знаковых чисел на «больше»?
Aja
Bjb
Cjg
Djc
3. Почему сложение не различает знак, а сравнение различает?
AСложение всегда беззнаковое
BДополнительный код делает сложение одинаковым, но «больше/меньше» зависит от трактовки старшего бита
CСравнение не использует флаги
DЭто случайная особенность NASM