Last Updated: 2023-10-02 14:19:01 Monday
-- TOC --
测试用C代码如下:
#include <stdio.h>
int main(){
int a = 0;
int b = 1;
float c = 2.3;
double d = 4.5;
char e = 'e';
short f = 2;
printf("%d %d %.2f %0.2f %c %d\n",
a, b, c, d, e, f);
return 7;
}
gcc 12.2,默认生成的汇编如下:(通过Compiler Explorer,它过滤掉了一些汇编器指令)
.LC2: # ASCII string, 自动补'\0'
.string "%d %d %.2f %0.2f %c %d\n"
main:
# prologue
push rbp
mov rbp, rsp
# expand stack frame with 32 bytes
# call main前,已经push了返回地址令入栈,
# 前面的push rbp后,rsp已经16bytes对齐,
# 再sub rsp, 32, 依然保持16bytes对齐!
sub rsp, 32
# int a = 0
mov DWORD PTR [rbp-4], 0
# int b = 1
mov DWORD PTR [rbp-8], 1
# 通过rip相对寻址得到.LC0的地址,
# 用DWORD PTR的方式读取这个地址,
# 将此地址开始的4bytes存入xmm0低32bit,
# xmm0高96bit被清零。(当movss的source为mem时)
movss xmm0, DWORD PTR .LC0[rip]
# float c = 2.3
movss DWORD PTR [rbp-12], xmm0
# 将此地址开始的8bytes存入xmm0第64bit,
# xmm0高64bit被清零。(当movsd的source为mem时)
movsd xmm0, QWORD PTR .LC1[rip]
# double d = 4.5
# [rbp-24]是为了8bytes对齐
movsd QWORD PTR [rbp-24], xmm0
# 将字母e的ASCII编码101存入[rbp-25]的位置
mov BYTE PTR [rbp-25], 101
# 将立即数2存入[rbp-28]的位置
# [rbp-28]是为了2bytes对齐
mov WORD PTR [rbp-28], 2
# 将[rbp-28]地址开始的WORD,
# 以signed扩展方式,存入edi寄存器
movsx edi, WORD PTR [rbp-28]
# 将[rbp-25]地址开始的BYTE,
# 以signed扩展的方式,存入ecx寄存器
# printf的第6个参数,char e
movsx ecx, BYTE PTR [rbp-25]
# pakced xor,对xmm2寄存器清0
pxor xmm2, xmm2
# 将[rbp-12]地址开始的DWORD,4bytes,
# 转换成double,8bytes,存入xmm2寄存器低64位,
# xmm2高64位不受影响。
cvtss2sd xmm2, DWORD PTR [rbp-12]
# 将xmm2寄存器内容,存入rsi寄存器
movq rsi, xmm2
# 将[rbp-24]地址开始的QWORD,8bytes,
# 存入xmm0寄存器
movsd xmm0, QWORD PTR [rbp-24]
# 将[rbp-8]地址开始的DWORD,4bytes,
# 存入edx寄存器
# printf的第3个参数,b
mov edx, DWORD PTR [rbp-8]
# 将[rbp-4]地址开始的DWORD,4bytes,
# 存入eax寄存器
mov eax, DWORD PTR [rbp-4]
# copy edi寄存器到r8d,即r8的低32bit
# printf的第7个参数,short f
mov r8d, edi
# copy xmm0寄存器到xmm1寄存器
# printf的第5个参数,浮点数d
# move 16-byte aligned packed double-precision float
# 如果是mem操作数,必须16字节对齐
movapd xmm1, xmm0
# copy rsi寄存器到xmm0的低64位,
# printf的第4个参数,浮点数c,
# movq是一条SSE2指令,不是ATT格式。
movq xmm0, rsi
# copy eax to esi
# printf的第2个参数,a
mov esi, eax
# copy .LC2's address to edi
# memory model is FLAT
# printf的第1个参数
mov edi, OFFSET FLAT:.LC2
# copy 2 to eax,
# printf是variadic function,2表示参数中有2个浮点数
mov eax, 2
# 将下一条指令地址压栈,
# 跳转到printf的地址,在printf的最后使用ret弹出此地址
call printf
# return 7
mov eax, 7
# shorthand epilogue,等价于下面两条指令:
# mov rsp, rbp
# pop rbp
# 这行代码,恢复了rsp和rbp的值
leave
# pop return address, return to that address
ret
.LC0:
# float 2.3所占的4bytes的unsigned int数值
.long 1075000115
.LC1:
# double 4.5所占的8bytes的两次unsigned int解析的数值
.long 0
.long 1074921472
操作浮点数及xmm寄存器的指令,均来自SSE1或2指令集。
汇编器指令
.string
:定义字符串,自动不0,同.asciz
。.long
:定义DWORD,4bytes。更多汇编器指令,参考:x86-64汇编基础。
关于两个浮点数
#include <stdio.h>
int main(int argc, char **argv) {
float fv = 2.3;
double fd = 4.5;
printf("%u\n", *((unsigned int*)&fv));
printf("%u\n", *((unsigned int*)&fd));
printf("%u\n", *((unsigned int*)&fd+1));
return 0;
}
输出:
1075000115
0
1074921472
参数传递符合Linux Calling Convention
call printf时的传参,严格按照Linux Calling Convention进行。
rip寄存器相对寻址
这是x64新增的寻址方式,rip寄存器永远指向下一条指令的地址,自动被CPU更新,在x64下,可以read。本文测试代码中,取浮点数常量的两条汇编指令如下:
movss xmm0, DWORD PTR .LC0[rip]
...
movsd xmm0, QWORD PTR .LC1[rip]
...
编译成object文件后,查看这两条指令,变为:
movss xmm0,DWORD PTR [rip+0x0] # 1e <main+0x1e>
...
movsd xmm0,QWORD PTR [rip+0x0] # 2b <main+0x2b>
...
所以,汇编指令写成.LC0[rip]
和.LC1[rip]
,只是告诉汇编器,用rip相对寻址的方式,计算到两个Label的地址。
编译后显示[rip+0x0]
,显然这里需要重定位。后面的注释表示执行到这条语句的时候,rip寄存器的值,即下一条指令的虚拟地址。
编译成可执行二进制文件后,这两条指令变成:
movss xmm0,DWORD PTR [rip+0xee8] # 40202c <__dso_handle+0x24>
...
movsd xmm0,QWORD PTR [rip+0xedf] # 402030 <__dso_handle+0x28>
...
地址和字长,__dso_handle是可执行文件中的一个符号,它的地址可用
readelf -s test
查看,value列就是各符号的虚拟地址。(如果是object文件,符号的value值一般表示在各自section中的offset,或size)
此时,所有地址都确定了。注释中是虚拟地址。这个区域属于.rodata:
$ readelf -S test
...
[16] .rodata PROGBITS 0000000000402000 00002000
0000000000000038 0000000000000000 A 0 0 8
...
查看这个区域:
$ readelf -x16 test
Hex dump of section '.rodata':
0x00402000 01000200 00000000 00000000 00000000 ................
0x00402010 25642025 6420252e 32662025 302e3266 %d %d %.2f %0.2f
0x00402020 20256320 25640a00 25750a00 33331340 %c %d..%u..33.@
0x00402030 00000000 00001240 .......@
0x40202c地址开始的4bytes为:33331340
0x402030地址开始的8bytes为:00000000 00001240
它们就是两个浮点数的值。把上面的那段代码稍微修改一下:
#include <stdio.h>
int main(int argc, char **argv) {
float fv = 2.3;
double fd = 4.5;
printf("%x\n", *((unsigned int*)&fv));
printf("%x\n", *((unsigned int*)&fd));
printf("%x\n", *((unsigned int*)&fd+1));
return 0;
}
输出:
40133333
0
40120000
代码中的浮点数常量,与字符串常量一样,进入.rodata,而不是立即数。
OFFSET FLAT
表示应该把其后跟着的符号虚拟地址(而不是内容)作为操作数。FLAT表示内存模型,简单地理解,就是取.LC2的虚拟地址。(早期还有分段内存模型)
mov edi, OFFSET FLAT:.LC2
只是地址。但为什么只用edi?
这段描述来自微软官网:Operations that output to a 32-bit subregister are automatically zero-extended to the entire 64-bit register. Operations that output to 8-bit or 16-bit subregisters are not zero-extended (this is compatible x86 behavior). 操作32位的寄存器,其高32位自动做0扩展,而且具有减小code size的好处。
gcc编译器默认是non-PIE模式,这个模式会将所有static code/data放在低2GiB的虚拟地址空间内,在64位CPU下,用32位的地址表示也足够。64 bit Linux uses the
small memory model
by default, which puts all code and static data below the 2GB address limit. This makes sure that you can use 32-bit absolute addresses.
这行代码编译后,最终会成为这样:
mov edi,0x402010
0x402010地址就是.rodata中的那段字符串的开始位置,如上。
如果采用-fPIE
编译选项,这行代码会变成:
lea rax, .LC2[rip]
mov rdi, rax
采用rip寄存器相对寻址,而且使用64位的rdi。
-O1下的汇编
.LC2:
.string "%d %d %.2f %0.2f %c %d\n"
main:
# call main已经将下一行指令的地址压栈了,
# 这里只需要在expand 8bytes,
# 就16bytes对齐了。
sub rsp, 8
mov r8d, 2
mov ecx, 101
movsd xmm1, QWORD PTR .LC0[rip]
movsd xmm0, QWORD PTR .LC1[rip]
mov edx, 1
mov esi, 0
mov edi, OFFSET FLAT:.LC2
mov eax, 2
call printf
mov eax, 7
add rsp, 8
ret
.LC0:
.long 0
.long 1074921472
.LC1:
.long 1610612736
.long 1073899110
-O1
优化后,只剩下直接给printf传参的代码了,全部使用立即数,无需寄存器在内存和stack(也是内存)之间中转数据。
修改rsp的两行代码,是为了保证rsp能够16字节对齐。这是Linux ABI的规定。如果没有这两行,直接Segmentation Fault。
-O2和-O3下的汇编
.LC2:
.string "%d %d %.2f %0.2f %c %d\n"
main:
sub rsp, 8
mov ecx, 101
mov edx, 1
xor esi, esi
mov r8d, 2
mov edi, OFFSET FLAT:.LC2
mov eax, 2
movsd xmm1, QWORD PTR .LC0[rip]
movsd xmm0, QWORD PTR .LC1[rip]
call printf
mov eax, 7
add rsp, 8
ret
.LC0:
.long 0
.long 1074921472
.LC1:
.long 1610612736
.long 1073899110
对esi寄存器的操作发生了变化,由mov变成了xor,只应该是应为mov的值是0导致的。
本文链接:https://cs.pynote.net/hd/asm/202302081/
-- EOF --
-- MORE --