将无符号字符转换为有符号数据类型时,为什么在汇编中使用movzbl? [英] Why movzbl is used in assembly when casting unsigned char to signed data types?

查看:165
本文介绍了将无符号字符转换为有符号数据类型时,为什么在汇编中使用movzbl?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在学习汇编中的数据移动( 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;返回* 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//移动%ax,(%rsi)//typedef unsigned char src_t;//typedef int dst_t;//movzbl(%rdi),%eax//move%eax,(%rsi)//typedef unsigned char src_t;//typedef long dst_t;//movzbl(%rdi),%eax//movq%rax,(%rsi) 

我想知道为什么在每种情况下都使用 movzbl 吗?它不应该与 dst_t 对应吗?谢谢!

解决方案

如果您想知道为什么 movzbw(%rdi),%ax 不是 short ,这是因为写入8位和16位部分寄存器必须与先前的高字节合并.

编写一个像EAX这样的32位寄存器会隐式零扩展到整个RAX中,从而避免了对RAX的旧值或任何ALU合并uop的错误依赖.( movzbl 或 movsbl ,与在RISC机器(如ARM ldrb ldrsb ,或MIPS lbu / lb .

GCC通常避免的怪异CISC事情是与仅替换低位(例如 movb(%rdi),%al )的旧值合并.为什么GCC不使用部分寄存器?通常会编写部分reg,而不仅仅是为商店阅读它们.您可能会看到clang仅加载到%al 中并存储了 dst_t signed char 的时间.


如果您想知道为什么不 movsbl(%rdi),%eax (符号扩展名)

source 值是无符号的,因此零扩展(不是符号扩展)是根据C语义扩展它的正确方法.要获取 movsbl ,您需要 return(int)(signed char)c .

* dp =(dst_t)* sp; 中,对 dst_t 的强制转换已经隐含在对 * dp 的赋值中.

>

无符号字符的值范围是0..255(在x86上,其中CHAR_BIT = 8).

将其零扩展为 signed int 可以产生一个范围为 0..255 的值,即,将每个值都保留为带符号的非负整数.

将其符号扩展为 signed int 将产生一个 -128 .. + 127 范围内的值,从而更改 unsigned char 的值值> =128.这与C语义冲突,无法扩展保留值的转换.


它是否对应于 dst_t ?

它必须至少加宽 dst_t .事实证明,使用 movzbl 扩展到64位(最上面的32位由隐式零扩展编写32位reg处理)是最有效的扩展方法.

存储到 * dp 是一个很好的演示,该示例说明了asm用于宽度不是32位的 dst_t .

无论如何,请注意只有一次转换发生.使用加载指令将您的 src_t 转换为al/ax/eax/rax中的 dst_t ,并存储到任意宽度的dst_t中.并留在那里作为返回值.

即使您只是要读取结果的低字节,零扩展负载也是正常的.

I'm learning data movement(MOV) in assembly.
I tried to compile some code to see the assembly in a x86_64 Ubuntu 18.04 machine:

typedef unsigned char src_t;
typedef xxx dst_t;

dst_t cast(src_t *sp, dst_t *dp) {
    *dp = (dst_t)*sp;
    return *dp;
}

where src_t is unsigned char. As for the dst_t, I tried char, short, int and long. The result is shown below:

// 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)

I wonder why movzbl is used in every case? Shouldn't it correspond to dst_t? Thanks!

解决方案

If you're wondering why not movzbw (%rdi), %ax for short, that's because writing to 8-bit and 16-bit partial registers has to merge with the previous high bytes.

Writing a 32-bit register like EAX implicitly zero-extends into the full RAX, avoiding a false dependency on the old value of RAX or any ALU merging uop. (Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?)

The "normal" way to load a byte on x86 is with movzbl or movsbl, same as on a RISC machine like ARM ldrb or ldrsb, or MIPS lbu / lb.

The weird-CISC thing that GCC usually avoids is a merge with the old value that replaces only the low bits, like movb (%rdi), %al. Why doesn't GCC use partial registers? Clang is more reckless and will more often write partial regs, not just read them for stores. You might well see clang load into just %al and store when dst_t is signed char.


If you're wondering why not movsbl (%rdi), %eax (sign-extension)

The source value is unsigned, therefore zero-extension (not sign-extension) is the correct way to widen it according to C semantics. To get movsbl, you'd need return (int)(signed char)c.

In *dp = (dst_t)*sp; the cast to dst_t is already implicit from the assignment to *dp.


The value-range for unsigned char is 0..255 (on x86 where CHAR_BIT = 8).

Zero-extending this to signed int can produce a value range from 0..255, i.e. preserving every value as signed non-negative integers.

Sign-extending this to signed int would produce a value range from -128..+127, changing the value of unsigned char values >= 128. That conflicts with C semantics for widening conversions preserving values.


Shouldn't it correspond to dst_t?

It has to widen at least as wide as dst_t. It turns out that widening to 64-bit by using movzbl (with the top 32 bits handled by implicit zero-extension writing a 32-bit reg) is the most efficient way to widen at all.

Storing to *dp is a nice demo that the asm is for a dst_t with a width other than 32-bit.

Anyway, note that there's only one conversion happening. Your src_t gets converted to dst_t in al/ax/eax/rax with a load instruction, and stored to dst_t of whatever width. And also left there as the return value.

A zero-extending load is normal even if you're just going to read the low byte of that result.

这篇关于将无符号字符转换为有符号数据类型时,为什么在汇编中使用movzbl?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆