碰到一个问题:
#include <stdio.h>
#include <stdint.h>
int main()
{
uint64_t a = 0;
uint8_t b = 0xff;
a = b << 24;
printf("a = 0x%lx\\n", a);
}
乍一看, a 的值应该是 0x0000 0000 ff00 0000,
然而在 X86_64 gcc 上 实际编译后,却是输出 0xffff ffff ff00 0000。
这里的问题是,b 的长度为 8 位,在 C 语言标准中,对于移位长度超过变量本身长度的操作是未定义的,所以可能导致不可预期的结果。
具体到汇编指令上,可以看到,objdump -dS 反汇编后的结果如下:
int main()
{
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
uint64_t a = 0;
1155: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
115c: 00
uint8_t b = 0xff;
115d: c6 45 f7 ff movb $0xff,-0x9(%rbp)
a = b << 24;
1161: 0f b6 45 f7 movzbl -0x9(%rbp),%eax
1165: c1 e0 18 shl $0x18,%eax
1168: 48 98 cltq
116a: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
116e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1172: 48 89 c6 mov %rax,%rsi
1175: 48 8d 3d 88 0e 00 00 lea 0xe88(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
117c: b8 00 00 00 00 mov $0x0,%eax
1181: e8 ca fe ff ff callq 1050 <printf@plt>
1186: b8 00 00 00 00 mov $0x0,%eax
}
可以看到,在使用 shl 指令将 eax 寄存器的值移位 24 位后,又调用 cltq 指令,该指令会对 将通过符号扩展的方式将 eax —> rax,也就是 32 位扩展为 64 位。这时候由于 eax 最高位为 1, 故被扩展为 0xffff ffff ff00 0000。
至于具体为什么这么编译,还有待探究。
继续测试:
#include <stdio.h>
#include <stdint.h>
int main()
{
uint64_t a = 0;
uint8_t b = 0xff;
uint32_t c = 0xff;
a = b << 24;
printf("a = 0x%lx\\n", a);
a = b << 3;
printf("a = 0x%lx\\n", a);
a = b;
printf("a = 0x%lx\\n", a);
a = c << 24;
printf("a = 0x%lx\\n", a);
a = c << 32;
printf("a = 0x%lx\\n", a);
}
反汇编后为:
int main()
{
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
uint64_t a = 0;
1155: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
115c: 00
uint8_t b = 0xff;
115d: c6 45 f3 ff movb $0xff,-0xd(%rbp)
uint32_t c = 0xff;
1161: c7 45 f4 ff 00 00 00 movl $0xff,-0xc(%rbp)
a = b << 24;
1168: 0f b6 45 f3 movzbl -0xd(%rbp),%eax
116c: c1 e0 18 shl $0x18,%eax
116f: 48 98 cltq
1171: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
1175: 48 8b 45 f8 mov -0x8(%rbp),%rax
1179: 48 89 c6 mov %rax,%rsi
117c: 48 8d 3d 81 0e 00 00 lea 0xe81(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1183: b8 00 00 00 00 mov $0x0,%eax
1188: e8 c3 fe ff ff callq 1050 <printf@plt>
a = b << 3;
118d: 0f b6 45 f3 movzbl -0xd(%rbp),%eax
1191: c1 e0 03 shl $0x3,%eax
1194: 48 98 cltq
1196: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
119a: 48 8b 45 f8 mov -0x8(%rbp),%rax
119e: 48 89 c6 mov %rax,%rsi
11a1: 48 8d 3d 5c 0e 00 00 lea 0xe5c(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
11a8: b8 00 00 00 00 mov $0x0,%eax
11ad: e8 9e fe ff ff callq 1050 <printf@plt>
a = b;
11b2: 0f b6 45 f3 movzbl -0xd(%rbp),%eax
11b6: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
11ba: 48 8b 45 f8 mov -0x8(%rbp),%rax
11be: 48 89 c6 mov %rax,%rsi
11c1: 48 8d 3d 3c 0e 00 00 lea 0xe3c(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
11c8: b8 00 00 00 00 mov $0x0,%eax
11cd: e8 7e fe ff ff callq 1050 <printf@plt>
a = c << 24;
11d2: 8b 45 f4 mov -0xc(%rbp),%eax
11d5: c1 e0 18 shl $0x18,%eax
11d8: 89 c0 mov %eax,%eax
11da: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
11de: 48 8b 45 f8 mov -0x8(%rbp),%rax
11e2: 48 89 c6 mov %rax,%rsi
11e5: 48 8d 3d 18 0e 00 00 lea 0xe18(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
11ec: b8 00 00 00 00 mov $0x0,%eax
11f1: e8 5a fe ff ff callq 1050 <printf@plt>
a = c << 32;
11f6: ba 20 00 00 00 mov $0x20,%edx
11fb: 8b 45 f4 mov -0xc(%rbp),%eax
11fe: 89 d1 mov %edx,%ecx
1200: d3 e0 shl %cl,%eax
1202: 89 c0 mov %eax,%eax
1204: 48 89 45 f8 mov %rax,-0x8(%rbp)
printf("a = 0x%lx\\n", a);
1208: 48 8b 45 f8 mov -0x8(%rbp),%rax
120c: 48 89 c6 mov %rax,%rsi
120f: 48 8d 3d ee 0d 00 00 lea 0xdee(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1216: b8 00 00 00 00 mov $0x0,%eax
121b: e8 30 fe ff ff callq 1050 <printf@plt>
1220: b8 00 00 00 00 mov $0x0,%eax
}
可以看出:
- 对于 8 位到 64 位的移位赋值,即使是移位数小于 变量长度,也依然会调用 cltq;
- 如果是单纯的赋值,从 8 位到 64 位并不会调用 cltq;
- 但如果是 32 到 64 位的移位赋值,不管移位长度是否超过变量长度,都不会调用 cltq。