Я собирался опубликовать ответ о sar / shr / add / и / sub части этого кода на Обратный инженерный код, который делает SAR на 31, shr, затем добавил и вычел это вокруг AND но он был удален примерно за минуту до того, как я закончил. Эта часть кода, вероятно, всплывает достаточно часто, чтобы стоило ответить:
Код вокруг AND выполняет n %= 16;
для 32-битного целого типа со знаком (например, int
), реализуя C семантику остатка со знаком. Для неотрицательных целых чисел это эквивалентно n &= 0xf;
, т. Е. Сохраняются только младшие 4 бита.
Вы можете заставить GCC4.7 испускать именно этот код ( Обозреватель компилятора Godbolt ) для этой функции:
int mod16(int n) {
return n % 16;
}
# gcc4.7.4 -O1 -m32 -mregparm=1 (first arg in EAX instead of on the stack)
mod16(int):
movl %eax, %edx
sarl $31, %edx
shrl $28, %edx
addl %edx, %eax
andl $15, %eax
subl %edx, %eax
ret # return with n%16 in EAX.
Интересный факт: gcc4.8 и новее используют cdq
вместо mov/sar $31
для подписания расширения EAX в EDX : EAX. т.е. установить все биты EDX = знаковый бит EAX.
Сохранение / перезагрузка в вашем коде и использование EBP в качестве указателя кадра указывают, что он был скомпилирован без (дополнительной) оптимизации. G CC больше, чем большинство компиляторов по-прежнему часто используют эффективные способы выполнения действий в одном выражении. Я использовал -O1
на Godbolt, чтобы избежать сохранения / перезагрузки компилятором памяти.
В C, %
должен произвести отрицательный остаток для отрицательного n
, но он все еще гораздо эффективнее эмулировать это вокруг инструкции AND для реализации семантики деления со знаком-целым числом, чем использовать idiv
для степеней 2. (То же самое для не степеней 2 констант времени компиляции с мультипликативным обратным .)
Напомним, что (n/16)*16 + n%16 = n
для всех n
и что C деление со знаком усекается до 0. (Это относится к любому делителю, а не только к положительным степеням 2). Так например -22/16 = -1
и -22 % 16 = -6
.
-1 * 16 + -6 = -22
. Компилятор должен выдавать код, который работает для всех возможных n
; он (обычно) не знает, что ваш int
никогда не бывает отрицательным при выполнении кода.
Arithmeti c сдвиг вправо в сторону -Inf, поэтому для реализации семантики /
необходимы аналогичные исправления.
Вот почему вы должны использовать unsigned
или n &= 15;
, когда вам не нужно не нужны отрицательные остатки, и вы хотите, чтобы он эффективно компилировался вместо этого слишком сложного sequence. Это дополнительное искажение вокруг and $0xf, %eax
может оптимизировать, если компилятор может доказать, что значение неотрицательно, но это не всегда может происходить (и никогда в отладочной сборке).
Это часть кода из чужого вопроса пару часов go, поэтому мы можем предоставить недостающую первую инструкцию, которая загружает EAX из -0x10(%ebp)
804847d: 8b 45 f0 mov -0x10(%ebp),%eax # EAX = n; you left out this insn
8048480: 89 c2 mov %eax,%edx
8048482: c1 fa 1f sar $0x1f,%edx # edx = 0 or -1 (sign bit of n)
8048485: c1 ea 1c shr $0x1c,%edx # edx = 0 or 15
Логическое смещение вправо на 28 всегда смещается в нули вместо копий знакового бита, так что -1
(0xffffffff) превращается в 15
. (32-28 = 4 бита осталось внизу регистра). И, конечно, оставляет 0 как 0.
Таким образом, EDX = 0 или 15, для неотрицательных или отрицательных n
.
8048488: 01 d0 add %edx,%eax
804848a: 83 e0 0f and $0xf,%eax
# only look at the lowest four bits
804848d: 29 d0 sub %edx,%eax
Это добавляет / вычитает 15 (или 0) вокруг AND.
Это оптимизированный способ сделать -((-eax) & 15)
для отрицательных входов, основанный на идентичности дополнения 2, которая -x = ~x + 1
в сочетании с &
.
Обратите внимание, что x & 0xf
приводит к числу в диапазоне [0..15], так что минус 15 всегда будет отрицательным или нулевым. Если входное значение уже было кратно 16 (даже если оно отрицательное, потому что так работает дополнение 2), младшие биты будут начинаться с 0. Так (n + 15) & 15 = 15 и 15-15 = 0, например, правильный результат для -128 % 16
.
Для первоначально отрицательных входных данных это просто добавление / вычитание 0, аддитивная идентичность, которая оставляет значение без изменений. Таким образом, все это эквивалентно n & 0xf
для неотрицательных n
.
Все это эквивалентно (eax < 0) ? -((-eax) & 15) : (eax & 15)
G CC, использующим его также для n % 16
показывает, что он дает правильный результат для всех возможных int
. (Нет значений n
, которые имеют C неопределенное поведение для n % 16
).
804848f: 89 45 f0 mov %eax,-0x10(%ebp) # store back into where we loaded from
Итак, мы знаем, что это n %= 16;
, потому что он сохраняет результат на вершине слота стека, из которого он загружен изначально.
8048492: 83 7d f0 03 cmpl $0x3,-0x10(%ebp)
# compare to 3
Да, все верно. Флаги установлены в соответствии с n - 3
, поскольку синтаксис AT & T задом наперед. Но ZF устанавливается, если они равны.