Почему movzbl используется в сборке при приведении unsigned char к подписанным типам данных? - PullRequest
2 голосов
/ 24 октября 2019

Я изучаю движение данных (MOV) в сборке.
Я попытался скомпилировать некоторый код, чтобы увидеть сборку на компьютере с Ubuntu 18.04 x86_64:

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 Ответ

3 голосов
/ 24 октября 2019

Если вам интересно, почему бы не movzbw (%rdi), %ax для short, то это потому, что запись в 8-битные и 16-битные частичные регистры должна сливаться с предыдущими старшими байтами.

Запись 32-битного регистра, такого как EAX, неявно распространяется на ноль в полный RAX, избегая ложной зависимости от старого значения RAX или любого слияния ALU. ( Почему инструкции x86-64 для 32-битных регистров обнуляют верхнюю часть полного 64-битного регистра? )

«Нормальный» способ загрузки байта наx86 с movzbl или movsbl, так же, как на RISC-машине, такой как ARM ldrb или ldrsb, или MIPS lbu / lb.

странный CISCGCC обычно избегает слияния со старым значением, которое заменяет только младшие биты, например movb (%rdi), %al. Почему GCC не использует частичные регистры? Clang более безрассуден и будет чаще писать частичные регистры, а не просто читать их для магазинов. Вы можете хорошо видеть, что лязг загружается в %al и сохраняется, когда dst_t равен signed char.


Если вам интересно, почему бы не movsbl (%rdi), %eax (знак-расширение)

Значение source не имеет знака, поэтому расширение нуля (не расширение знака) является правильным способом его расширения в соответствии с семантикой 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 любой ширины. И также остается там как возвращаемое значение.

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

...