Почему 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 любой ширины. И тоже оставил там как возвращаемое значение.

Нагрузка с нулевым расширением - это нормально, даже если вы просто собираетесь читать младший байт этого результата.

Другие вопросы по тегам