Почему movzbl используется в сборке при преобразовании unsigned char в подписанные типы данных?
Я изучаю перемещение данных (MOV
) в сборке.
Я попытался скомпилировать код, чтобы увидеть сборку на машине x86_64 Ubuntu 18.04:
typedef unsigned char src_t;
typedef xxx dst_t;
dst_t cast(src_t *sp, dst_t *dp) {
*dp = (dst_t)*sp;
return *dp;
}
где src_t
является unsigned char
. Дляdst_t
, Я попытался char
, short
, int
а также long
. Результат показан ниже:
// typedef unsigned char src_t;
// typedef char dst_t;
// movzbl (%rdi), %eax
// movb %al, (%rsi)
// typedef unsigned char src_t;
// typedef short dst_t;
// movzbl (%rdi), %eax
// movw %ax, (%rsi)
// typedef unsigned char src_t;
// typedef int dst_t;
// movzbl (%rdi), %eax
// movl %eax, (%rsi)
// typedef unsigned char src_t;
// typedef long dst_t;
// movzbl (%rdi), %eax
// movq %rax, (%rsi)
интересно, почему movzbl
используется в каждом случае? Разве это не должно соответствоватьdst_t
? Спасибо!
1 ответ
Если вам интересно, почему бы и нет movzbw (%rdi), %ax
за short
, потому что запись в 8-битные и 16-битные частичные регистры должна сливаться с предыдущими старшими байтами.
Запись 32-битного регистра, такого как EAX, неявно расширяет нулями до полного RAX, избегая ложной зависимости от старого значения RAX или любого объединяющего uop ALU. ( Почему инструкции x86-64 в 32-битных регистрах обнуляют верхнюю часть полного 64-битного регистра?)
"Нормальный" способ загрузки байта на x86 - это movzbl
или movsbl
, так же, как на RISC-машине, такой как ARM ldrb
или ldrsb
, или MIPS lbu
/ lb
.
Странная вещь с CISC, которую GCC обычно избегает, - это слияние со старым значением, которое заменяет только младшие биты, например movb (%rdi), %al
. Почему GCC не использует частичные регистры? Clang более безрассуден и будет чаще писать частичные регистры, а не просто читать их для магазинов. Вы могли бы увидеть, как clang загружается только в%al
и хранить, когда dst_t
является signed char
.
Если вам интересно, почему бы и нет movsbl (%rdi), %eax
(знак-расширение)
Источник значение без знака, поэтому нулевое расширения (не знак-расширение) является правильным способом, чтобы расширить его в соответствии с семантикой C. Получитьmovsbl
вам понадобится return (int)(signed char)c
.
В *dp = (dst_t)*sp;
бросок в dst_t
уже подразумевается из присвоения *dp
.
Диапазон значений для unsigned char
0..255 (на x86, где CHAR_BIT = 8).
Нулевое расширение до signed int
может производить диапазон значений от 0..255
, т.е. сохранение каждого значения в виде неотрицательных целых чисел со знаком.
Знак, распространяющий это на signed int
даст диапазон значений от -128..+127
, изменяя значение unsigned char
values >= 128. Это противоречит семантике C для расширения преобразований с сохранением значений.
Разве это не должно соответствовать
dst_t
?
Он должен расширяться не менее чемdst_t
. Оказывается, расширение до 64-битной версии с помощьюmovzbl
(с верхними 32 битами, обрабатываемыми неявной записью 32-битного регистра с нулевым расширением) - самый эффективный способ вообще расширения.
Хранение в *dp
хорошая демонстрация того, что asm предназначен для dst_t
с шириной, отличной от 32-битной.
В любом случае, обратите внимание, что происходит только одно преобразование. Твойsrc_t
конвертируется в dst_t
в al/ax/eax/rax с инструкцией загрузки и сохраняется в dst_t любой ширины. И тоже оставил там как возвращаемое значение.
Нагрузка с нулевым расширением - это нормально, даже если вы просто собираетесь читать младший байт этого результата.