接上一篇的内容,继续来讨论格式化漏洞的相关知识点!
为什么,当使用特别的方式向 printf 函数传递参数时,会可能导致栈结构空间中的内容被暴露呢?
因为, printf 函数默认读取与显示的是栈结构空间中的相应内容(而不管进程的调用者是否向 printf 函数中传递了正确的内容)!这就是格式化漏洞产生的重要原因!
我们知道,根据C语言函数调用栈空间数据的规则,在栈结构空间中,第一个局部变量的位置( ebp - 4 ,或 rbp - 8 )的上面位置(ebp 或 rbp)中存储的是当前函数被调用之前的EBP或RBP寄存器中的内容值,而栈结构空间中的EBP或RBP寄存器内容值的位置向上再偏移4或8个字节( ebp + 4 ,或 rbp + 8 ),将是函数返回地址所处的栈结构空间位置!
在32位环境中,再向上(高位地址方向)偏移4个字节(ebp + 8),就是第一个函数参数所处的栈空间位置(根据32位环境下,C语言函数的栈调用规则,函数的最后一个参数将被最先入栈,函数的第一个参数将被最后入栈,入栈方式为:高位地址向低位地址每次递减4个字节)。
在64位环境中,再向上(高位地址方向)偏移8个字节(rbp + 16),如果第七个函数参数并不是浮点型数据(浮点型数据,会用专门的XMM系列寄存器进行存储),那么(rbp + 16)的位置存储的就是第七个函数参数的内容值!
将C语言的函数代码转换为汇编语言的函数代码后,基本会经历下面的过程:
1、函数的序言代码部分
push ebp 或 push rbp
mov ebp,esp 或 mov rbp,rsp
sub ebp,<局部变量占用的空间大小(以字节为单位),空间大小为2的整数倍>
或
sub rbp,<局部变量占用的空间大小(以字节为单位),空间大小为2的整数倍>
2、函数的正式代码部分
…………………………………………………………………………………………
#正式的函数内容代码开始
#正式的函数内容代码结束
…………………………………………………………………………………………
3、函数的尾声代码部分
mov esp,ebp 或 mov rsp,rbp
pop ebp 或 pop rbp
ret 0
为什么要这样做呢?为什么要在正式的函数内容代码部分的前后,去分别加上函数序言代码部分和函数尾声代码部分呢?答案很简单,为了栈平衡!想要学习二进制安全,想要挖掘二进制程序的安全漏洞,那么这些知识是必须要掌握的!关于栈平衡的概念,可以在一些操作系统的汇编语言教程中,或者在一些专业的逆向工程课程中,找到一些相关的知识点内容。想要挖掘二进制安全领域的0day漏洞,对于栈数据结构的了解,对于栈平衡概念的深入理解,对于C语言的栈调用规则的清晰理解,都是必须要做到的!这是基础中的基础!想要研究格式化漏洞,你首先要了解“栈”的领域的相关知识,你必须要知道“栈”中存储了哪些东西!你必须要明白EBP和RBP寄存器的重要意义与价值体现!你必须要明白ESP和RSP寄存器起到了非常关键的栈指针偏移作用!你必须要知道比较常用的操纵ESP和RSP寄存器的汇编指令都有哪些?!你必须记住这几条汇编指令,它们分别是MOV、XOR、SUB、ADD、PUSH、POP、CALL、RET、JMP、CMP、TEST、JZ、JNZ。你必须知道 dword ptr 和 qword ptr 的用途!什么是近端转移?什么是远端转移?什么是相对偏移?什么是绝对偏移?这些都是基础知识!都是进行二进制安全领域的漏洞挖掘所必须具备的基础知识!
你还要了解标志寄存器 FLAGS ,里面的标志位都很关键!几乎每一个标志位,在进行OD调试(动态调试)时都会被经常性用到(比较关键的标志位为进位标志CF、奇偶标志PF、辅助进位标志AF、零标志ZF、符号标志SF、溢出标志OF、追踪标志TF、中断允许标志IF、方向标志DF、输入输出标志IOPL、嵌套任务标志NT、恢复标志RF、虚拟8086方式标志VM等),几乎FLAGS寄存器中的所有标志位都有着非凡的意义和存在价值,都值得获得我们在时间与精力方面的足够投入!
在格式化漏洞的研究领域,你必须知道每一个格式化符号的用途!
1、格式化符号 “%d” 的作用是约定按照整型数据的实际长度来进行数据内容的输出;
2、格式化符号 “%c” 的作用是输出一个ASCII码字符(ASCII码字符,见ASCII码字符表内容);
3、格式化符号 “%s” 的作用是输出一个字符串(本质上,传递的是字符串的起始内存地址);
4、格式化符号 “%x” 的作用是以十六进制的整数形式来进行数据内容输出;
5、格式化符号 “%f” 的作用是约定按照浮点型数据的方式来进行数据内容的输出;
6、格式化符号 “%p” 的作用是约定按照指针类型的数据内容来进行内存地址的输出;
7、格式化符号 “%n” ,这是一个非常重要和特殊的格式化控制符号!这个特殊的控制符号值得被重点的讲一下!格式化符号 “%n” 的用途是将在“%n”符号出现之前的,已经由 printf 等类似的函数打印出来的字符的总个数,赋值给 printf 等类似的函数的相应参数(赋值给哪个参数与 printf 等类似函数传参的顺序有关,一般被进行赋值的函数参数的表示形式会比较特殊(这类参数的名称前面会有“&”(取地址)符号(例外的情况,这个函数参数本身就是一个指针变量)))!在进行格式化漏洞的挖掘与验证过程中,特殊的格式化符号 “%n”将会被经常性的被用到!
我们应该思考,“%n” 这个格式化符号是如何被使用的?
我们先来复习下 C语言的栈结构空间调用规则,这个是非常重要的知识点内容!想要了解“%n” 格式化符号在渗透测试行为中的使用,就必须要深刻透彻地了解相关细节。
在32位的系统环境下,我们已知:(ebp + 4)= 函数返回地址在栈结构空间中的内存地址;
在64位的系统环境下,我们已知:(rbp + 8)= 函数返回地址在栈结构空间中的内存地址;
32位和64位系统环境的不同入栈规则:
32位环境的入栈(栈指针偏移)方式:从高位内存地址向低位内存地址方向,每次递减 4个字节!
64位环境的入栈(栈指针偏移)方式:从高位内存地址向低位内存地址方向,每次递减 8个字节!
我们来进行公式推导(以32位的系统环境为例,64位环境的有些函数参数被存放在寄存器中):
(ebp + 4) + 4 = 第一个函数参数;
(ebp + 4) + (4 + 4) = 第二个函数参数;
(ebp + 4) + (4 + 4 + 4) = 第三个函数参数;
在第三个函数参数入栈之前,栈指针寄存器中的内容值为:(ebp + 4) + (4 + 4 + 4 + 4)
第三个函数参数,最先入栈(push function param 3);
栈偏移指针寄存器的内容值,被自动减少4个字节,变成了:(ebp + 4) + (4 + 4 + 4 );
第三个函数参数,被放入到了内存地址((ebp + 4) + (4 + 4 + 4))对应的内存空间上;
第二个函数参数,接着入栈(push function param 2);
栈偏移指针寄存器的内容值,被自动减少4个字节,变成了:(ebp + 4) + (4 + 4 );
第二个函数参数,被放入到了内存地址((ebp + 4) + (4 + 4))对应的内存空间上;
第一个函数参数,最后入栈(push function param 1);
栈偏移指针寄存器的内容值,被自动减少4个字节,变成了:(ebp + 4)+ 4 ;
第一个函数参数,被放入到了内存地址((ebp + 4) + 4)对应的内存空间上;
再然后,就是函数的返回地址入栈了(EIP寄存器的内容值入栈)。
栈偏移指针寄存器的内容值,被自动减少4个字节,变成了:(ebp + 4);
此时,函数的返回地址被写入到了内存地址(ebp + 4)对应的内存空间中!
轮到ebp(栈帧基址)寄存器的内容值入栈了!(rbp寄存器,通常被用于指向当前函数的栈帧的基址,也就是局部变量和函数参数的起始位置!ebp+4 指向函数的返回地址!ebp+8 指向第一个函数参数!ebp-4 通常被指向函数内部定义的第一个局部变量)
栈偏移指针寄存器的内容值,被自动减少4个字节,变成了:(ebp);
此时,ebp(栈帧基址)寄存器的内容值被写入到了内存地址(ebp)对应的内存空间中!
内存地址(ebp)对应的内存空间中,存储着当前函数被调用之前的上文环境中的EBP寄存器中的内容值!
再然后,通常会执行 sub esp,<函数内部的局部变量所占空间总大小> 这样的操作了!
注意:(ebp + n)这样的栈空间内容计算规则,是在执行完当前函数的函数序言代码位置的(push ebp)内容后生效的!
注意:(rbp + n)这样的栈空间内容计算规则,是在执行完当前函数的函数序言代码位置的(push rbp)内容后生效的!
以下面的函数调用举例:
printf( "%d %d" , param2 , param3 );
在32位的系统环境下,进行栈空间数据的存储分析:
param3 addr = ((ebp + 4) + (4 + 4 + 4));
param2 addr = ((ebp + 4) + (4 + 4));
简化后:
param3 addr = ((ebp + 4) + (3 * 4));
param2 addr = ((ebp + 4) + (2 * 4));
得出结论:
32-bit environment :format params(n) addr = (( ebp + 4 ) +(( n + 1 )* 4));
得出定理:
32位:第 n 个函数格式化参数的内存地址为 (( ebp + 4 ) +(( n + 1 )* 4))
公式:%n$p = 指向第 n 个函数格式化参数的内存地址(%10$p 对应第10个函数格式化参数);
我们先来看看下面的代码,这有助于我们更加深入的理解格式化符号的作用。
C语言代码:
char string[17] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','\0'};
char * string_point = string;
printf("\n");
printf("%p %p\n",(string),(string+1));
printf("%c %c\n",*(string),*(string+1));
printf("%p %p\n",(string_point),(string_point+1));
printf("%c %c\n",*(string_point),*(string_point+1));
C语言代码的执行结果:
0x7ffe6f4fdbb0 0x7ffe6f4fdbb1
0 1
0x7ffe6f4fdbb0 0x7ffe6f4fdbb1
0 1
执行结果引发的思考!
0x7ffe6f4fdbb0 是什么?
答案,是内存地址!
格式化符号 “ %p ” 用于输出内存地址类型的数据,其实就是 4字节(32位)或 8字节(64位)空间大小的十六进制数据!对于内存地址类型的数据进行输出时,一般用十六进制表示,这是种惯例!
0x7ffe6f4fdbb1 是什么?
答案,还是内存地址!
0x7ffe6f4fdbb0 是数组中第1个元素的内存地址,0x7ffe6f4fdbb1 是数组中第2个元素的内存地址!(string)代表数组第一个元素的内存地址(数组的首地址),(string+1)则代表数组第二个元素的内存地址!
输出 (string_point) 的值,为什么会与 (string) 的值相同?
答案,string_point 是指针变量,string 是数组名,string_point 和 string 共同指向栈结构空间中同一块内存区域的起始地址(局部变量的值,被存储在栈结构空间中)!
输出 *(string_point) 的值,为什么会与 *(string) 的值相同?
答案,既然string_point 和 string 共同指向栈结构空间中同一块内存区域的起始地址,那么 *(string_point) 和 *(string) 的值,自然也会相同!大家,可以把“数组名”理解为一块“内存空间”的“别名”!如同我们有些人,除了身份证上的姓名之外,还会存在“小名”或者“艺名”等其它的代名,是同样的道理!无论是数组名,还是指针变量,大家都可以把它们理解为是一块内存空间的别名!即使是多级指针,那么也同样的一块块内存空间的别名!例如,void *** point = NULL; 这个指针变量,除了 *** point 代表着内存区域上存放的数据之外,其它的表现形式,诸如 ** point 、* point 、point 这些,都是不同的内存空间的别名(当然,也可能是相同的内存空间。例如,指针变量 point 上存储的内存数据是指针变量 point 自己的内存地址!这是种比较特殊的例外情况)!
格式化符号 “ %c " 的用途是什么呢?
大部分格式化符号,除了起到占位的作用(用于未来在字符串内容替换时使用)之外,不同的格式化符号,还起着输出不同格式内容的作用!格式化符号 “ %c " 的用途是输出一个ASCII码字符!
大小写字母,阿拉伯数字,英文的标点符号,还有 TAB、空格、回车、换行等,都是ASCII码字符!代表字符串结束的 '\0' ,也是ASCII码字符!ASCII码字符的取值范围是 0~255(1字节的存储空间中,可以容纳的最大数字型数据)。
之后,我们如果再添加一行C语言代码:
printf("%s\n");
接着,我们再看一下这行C语言代码对应的反汇编代码:
lea rax, aS ; "%s\n"
mov rdi, rax ; format
mov eax, 0
call _printf
大家看到什么了吗?
我们把 "%s\n" 这个字符串的地址(aS,只是别名)写入到 rax 寄存器中,之后我们再把 rax 寄存器的内容写入到 rdi 寄存器(rdi 寄存器,用于存储函数的首个参数的内容值)中,之后 我们 call _printf(调用 _printf 函数)。
为什么是 _printf 函数,而不是 printf 函数呢?我们来看一下 _printf 函数的实现!
; Attributes: thunk
; int printf(const char *format, ...)
_printf proc near
jmp cs:off_4010
_printf endp
看到了吗?_printf 函数里,只是执行了一个 jmp 指令(无条件跳转指令),用于跳转到代码段中的指定位置!这个 _printf 函数 存储在 PLT(过程链接表)节中,是用来实现真正的 printf 函数调用(有关PLT和GOT表的更多信息,请参阅《Learning Linux Binary Analysis》(《Linux 二进制分析》)这本书,作者为 Ryan O'Neill 先生)!
CS 寄存器,是代码段基址寄存器!off_4010 是基于代码段基址的偏移量! cs:off_4010 这个内存地址(位于GOT节中)上存放的数据是printf 函数真正所在的位置(也就是 printf 函数代码在当前进程中的起始内存地址!也就是 printf 函数中首行汇编指令(这个指令,一般为:PUSH RBP,对应的机器码为:55,对应的shellcode代码为:\x55)的内存地址)!当 printf 函数对应的共享库被载入到当前进程空间之中, 并且 printf 函数的代码内容被正式调用之后,printf 函数的真实内存地址会被写入到 GOT(全局偏移表) 节中! PLT (过程链接表)节,主要用于调用从共享库中导入的函数(里面包含了 JMP 无条件跳转指令,用于跳转到 GOT(全局偏移表)节中存储的内存地址(位于代码段中)), GOT(全局偏移表) 节,主要用于存储共享库中的相应函数的真实内存地址!
这里,我们要科普一些小知识,介绍一下ELF格式文件中比较重要的相应节用途!
.text: 代码节,用于存储程序的可执行代码内容(访问权限为:只读);
.rodata:数据节,用于存储程序的全局常量内容(访问权限为:只读);
.data:数据节,用于存储已经进行初始化操作的全局变量(每个变量的名称、大小、初始值);
.bss : 数据节,用于存储没有进行初始化操作的全局变量(每个变量的名称、大小);
.plt:过程链接表,用于调用从共享库中导入的函数(访问权限为:只读);
.got:全局偏移表,用于存储共享库中相应函数在进程内的真实地址(在64位系统环境中,为RIP 相对寻址!真实内存地址需要通过RIP寄存器中的内存地址 + 当前指令的字节长度+GOT(全局偏移表)中对应存储的相对偏移量来进行计算获得)(非严格链接模式下的访问权限为:可读写,严格链接模式下的访问权限为:只读);
.dynsym:动态符号表,用于存储从共享库中导入的动态符号信息;
.dynstr:动态符号字符串表,用于存储从共享库中导入的动态符号的字符串信息;
.rel.*:重定位表,用于重定位相关信息的存储(描述如何在链接或者运行时,对ELF目标文件的某部分内容或者进程镜像进行补充或修改);
.hash:哈希表,保存了查找符号所需的散列表(哈希表,也被称为”散列表“);
.symtab:符号信息表,保存了ELF文件中的所有 ElfN_Sym (elf.h 文件中定义的一种数据结构)类型的符号信息;
.strtab:符号字符串表,保存着ELF文件中的所有符号的字符串信息(表内数据会被.symtab的ElfN_Sym结构中的st_name条目引用);
.shstrtab:节头字符串表,保存了所有节头的节头名称;
.ctors:构造器,保存了指向构造函数的函数指针;
.dtors:析构器,保存了指向析构函数的函数指针;
下面,我们再来深入分析一篇C语言代码,这对于我们深入理解格式化符号,以及函数参数的传递与存储将会有很大的帮助!
#include <stdio.h>
#include <memory.h>
void show(double double_tmp, char char_tmp) {
int char_total = 0;
printf("%f %c%n\n",double_tmp,char_tmp,&char_total);
printf("%d\n",char_total);
}
int main() {
//
double double_0 = 0.000000;
double double_1 = 1.000000;
double double_2 = 2.000000;
double double_3 = 3.000000;
double double_4 = 4.000000;
double double_5 = 5.000000;
double double_6 = 6.000000;
double double_7 = 7.000000;
double double_8 = 8.000000;
double double_9 = 9.000000;
//
char char_0 = '0';
char char_1 = '1';
char char_2 = '2';
char char_3 = '3';
char char_4 = '4';
char char_5 = '5';
char char_6 = '6';
char char_7 = '7';
char char_8 = '8';
char char_9 = '9';
//
int char_total = 0;
printf("%f %f %f %f %f %f %f %f %f %f %c %c %c %c %c %c %c %c %c %c%n\n", double_0, double_1, double_2, double_3,
double_4, double_5, double_6,
double_7, double_8, double_9, char_0, char_1, char_2, char_3, char_4, char_5, char_6, char_7, char_8, char_9,
&char_total);
printf("%d\n", char_total);
show(double_0,char_0);
return 0;
}
在上面的代码中,我们使用了浮点型数据、字符型数据、整数型数据!
在上面的代码中,我们使用了格式化符号”%f“、格式化符号”%c“、格式化符号”%n“!
现在,我们来看看反汇编代码的内容:
.rodata:0000000000002000 ; ===========================================================================
.rodata:0000000000002000
.rodata:0000000000002000 ; Segment type: Pure data
.rodata:0000000000002000 ; Segment permissions: Read
.rodata:0000000000002000 _rodata segment qword public 'CONST' use64
.rodata:0000000000002000 assume cs:_rodata
.rodata:0000000000002000 ;org 2000h
.rodata:0000000000002000 public _IO_stdin_used
.rodata:0000000000002000 _IO_stdin_used db 1 ; DATA XREF: LOAD:0000000000000130↑o
.rodata:0000000000002001 db 0
.rodata:0000000000002002 db 2
.rodata:0000000000002003 db 0
.rodata:0000000000002004 db 0
.rodata:0000000000002005 db 0
.rodata:0000000000002006 db 0
.rodata:0000000000002007 db 0
.rodata:0000000000002008 ; const char format[]
.rodata:0000000000002008 format db '%f %c%n',0Ah,0 ; DATA XREF: show+2C↑o
.rodata:0000000000002011 ; const char aD[]
.rodata:0000000000002011 aD db '%d',0Ah,0 ; DATA XREF: show+45↑o
.rodata:0000000000002011 ; main+167↑o
.rodata:0000000000002015 align 8
.rodata:0000000000002018 ; const char aFFFFFFFFFFCCCC[]
.rodata:0000000000002018 aFFFFFFFFFFCCCC db '%f %f %f %f %f %f %f %f %f %f %c %c %c %c %c %c %c %c %c %c%n',0Ah
.rodata:0000000000002018 ; DATA XREF: main+14A↑o
.rodata:0000000000002056 db 0
.rodata:0000000000002057 align 8
.rodata:0000000000002058 qword_2058 dq 3FF0000000000000h ; DATA XREF: main+16↑r
.rodata:0000000000002060 qword_2060 dq 4000000000000000h ; DATA XREF: main+23↑r
.rodata:0000000000002068 qword_2068 dq 4008000000000000h ; DATA XREF: main+30↑r
.rodata:0000000000002070 qword_2070 dq 4010000000000000h ; DATA XREF: main+3D↑r
.rodata:0000000000002078 qword_2078 dq 4014000000000000h ; DATA XREF: main+4A↑r
.rodata:0000000000002080 qword_2080 dq 4018000000000000h ; DATA XREF: main+57↑r
.rodata:0000000000002088 qword_2088 dq 401C000000000000h ; DATA XREF: main+64↑r
.rodata:0000000000002090 qword_2090 dq 4020000000000000h ; DATA XREF: main+71↑r
.rodata:0000000000002098 qword_2098 dq 4022000000000000h ; DATA XREF: main+7E↑r
.rodata:0000000000002098 _rodata ends
.rodata:0000000000002098
; Attributes: bp-based frame
; void __cdecl show(double double_tmp, char char_tmp)
public show
show proc near
char_tmp= byte ptr -1Ch
double_tmp= qword ptr -18h
char_total= dword ptr -4
; __unwind {
push rbp
mov rbp, rsp
sub rsp, 20h
movsd [rbp+double_tmp], xmm0
mov eax, edi
mov [rbp+char_tmp], al
mov [rbp+char_total], 0
movsx ecx, [rbp+char_tmp]
lea rdx, [rbp+char_total]
mov rax, [rbp+double_tmp]
mov esi, ecx
movq xmm0, rax
lea rax, format ; "%f %c%n\n"
mov rdi, rax ; format
mov eax, 1
call _printf
mov eax, [rbp+char_total]
mov esi, eax
lea rax, aD ; "%d\n"
mov rdi, rax ; format
mov eax, 0
call _printf
nop
leave
retn
; } // starts at 1139
show endp
; Attributes: bp-based frame
; int __fastcall main(int argc, const char **argv, const char **envp)
public main
main proc near
char_total= dword ptr -80h
char_9= byte ptr -7Ah
char_8= byte ptr -79h
char_7= byte ptr -78h
char_6= byte ptr -77h
char_5= byte ptr -76h
char_4= byte ptr -75h
char_3= byte ptr -74h
char_2= byte ptr -73h
char_1= byte ptr -72h
char_0= byte ptr -71h
double_9= qword ptr -70h
double_8= qword ptr -68h
double_7= qword ptr -60h
double_6= qword ptr -58h
double_5= qword ptr -50h
double_4= qword ptr -48h
double_3= qword ptr -40h
double_2= qword ptr -38h
double_1= qword ptr -30h
double_0= qword ptr -28h
; __unwind {
push rbp
mov rbp, rsp
push r13
push r12
push rbx
sub rsp, 68h
pxor xmm0, xmm0
movsd [rbp+double_0], xmm0
movsd xmm0, cs:qword_2058
movsd [rbp+double_1], xmm0
movsd xmm0, cs:qword_2060
movsd [rbp+double_2], xmm0
movsd xmm0, cs:qword_2068
movsd [rbp+double_3], xmm0
movsd xmm0, cs:qword_2070
movsd [rbp+double_4], xmm0
movsd xmm0, cs:qword_2078
movsd [rbp+double_5], xmm0
movsd xmm0, cs:qword_2080
movsd [rbp+double_6], xmm0
movsd xmm0, cs:qword_2088
movsd [rbp+double_7], xmm0
movsd xmm0, cs:qword_2090
movsd [rbp+double_8], xmm0
movsd xmm0, cs:qword_2098
movsd [rbp+double_9], xmm0
mov [rbp+char_0], 30h ; '0'
mov [rbp+char_1], 31h ; '1'
mov [rbp+char_2], 32h ; '2'
mov [rbp+char_3], 33h ; '3'
mov [rbp+char_4], 34h ; '4'
mov [rbp+char_5], 35h ; '5'
mov [rbp+char_6], 36h ; '6'
mov [rbp+char_7], 37h ; '7'
mov [rbp+char_8], 38h ; '8'
mov [rbp+char_9], 39h ; '9'
mov [rbp+char_total], 0
movsx r11d, [rbp+char_9]
movsx r10d, [rbp+char_8]
movsx r9d, [rbp+char_7]
movsx r8d, [rbp+char_6]
movsx edi, [rbp+char_5]
movsx r13d, [rbp+char_4]
movsx r12d, [rbp+char_3]
movsx ecx, [rbp+char_2]
movsx edx, [rbp+char_1]
movsx esi, [rbp+char_0]
movsd xmm6, [rbp+double_7]
movsd xmm5, [rbp+double_6]
movsd xmm4, [rbp+double_5]
movsd xmm3, [rbp+double_4]
movsd xmm2, [rbp+double_3]
movsd xmm1, [rbp+double_2]
movsd xmm0, [rbp+double_1]
mov rax, [rbp+double_0]
lea rbx, [rbp+char_total]
push rbx
push r11
push r10
push r9
push r8
push rdi
push [rbp+double_9]
push [rbp+double_8]
mov r9d, r13d
mov r8d, r12d
movapd xmm7, xmm6
movapd xmm6, xmm5
movapd xmm5, xmm4
movapd xmm4, xmm3
movapd xmm3, xmm2
movapd xmm2, xmm1
movapd xmm1, xmm0
movq xmm0, rax
lea rax, aFFFFFFFFFFCCCC ; "%f %f %f %f %f %f %f %f %f %f %c %c %c "...
mov rdi, rax ; format
mov eax, 8
call _printf
add rsp, 40h
mov eax, [rbp+char_total]
mov esi, eax
lea rax, aD ; "%d\n"
mov rdi, rax ; format
mov eax, 0
call _printf
movsx edx, [rbp+char_0]
mov rax, [rbp+double_0]
mov edi, edx ; char_tmp
movq xmm0, rax ; double_tmp
call show
mov eax, 0
lea rsp, [rbp-18h]
pop rbx
pop r12
pop r13
pop rbp
retn
; } // starts at 1195
main endp
_text ends
发现了什么吗?在64位环境下,第一个函数参数通常会被存储于RDI寄存器中,但是如果第一个参数是浮点型数据呢?那么意味着,这个惯例将会被打破!
浮点型的函数参数使用xmm系列的寄存器来进行存储,如果函数第一个参数是浮点型数据,那么将被使用xmm0寄存器来进行数据存储!
这里,我们可以总结一个规律!
在64位的系统环境下,如果函数参数中不存在浮点型数据,那么,将使用通常的惯例方式进行函数参数的传递与存储!惯例方式为:
MSVC编译器中,会使用RCX、RDX、R8、R9寄存器来传递前四个函数参数,其它的函数参数仍然会利用栈空间结构以“先进后出”的方式进行函数参数传递!
GCC编译器中,会使用RDI、RSI、RDX、RCX、R8、R9寄存器来传递前六个函数参数,其它的函数参数仍然会利用栈空间结构以“先进后出”的方式进行函数参数传递!
在64位的系统环境下,如果函数参数中存在浮点型数据,那么,将使用非惯例的方式进行函数参数的传递与存储!浮点型数据,一般会使用xmm系列寄存器进行存储!xmm系列寄存器的优先使用顺序,一般为XMM0、XMM1、XMM2、XMM3、XMM4、XMM5、XMM6、XMM7、XMM8!非浮点型数据,函数参数的寄存器优先使用顺序,一般为RDI、RSI、RDX、RCX、R8、R9寄存器!对于更多的函数参数,将仍然使用栈结构空间进行存储!