Знаковые и беззнаковые операции
Урок показывает, где знаковость реально влияет на команды: расширение, переходы и переполнение.
Знаковость — это не свойство битов, а решение программиста, как их трактовать; от него зависят некоторые команды.
Где машина всё-таки различает знак
Сложение и вычитание одинаковы для знаковых и беззнаковых — спасибо дополнительному коду. Но в трёх местах разница принципиальна: при расширении числа в больший регистр, при сравнении и переходах и при делении/умножении.
Расширение: 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), биты сами по себе нейтральны.