[音乐] 最后我们通过一个例子来看一下
IA-32和x86-64有什么不同
这个例子当中实际上只有一条赋值语句,加上一个printf的函数调用
这个程序在32位机器上运行的时候,就是在IA-32上运行的时候
实际上这边printf打印出来的这个结果呢是a=0 而在x86-64机器上运行的时候
打印出来的这个a是一个不确定的值 为什么在两种不同的平台上它们打印的结果不一样呢?
而且为什么它们分别是打印出来的一个等于0一个是不确定值呢?
我们首先来看一下在IA-32架构上面它打印出来的情况
首先我们看一下这个10在机器里面的表示 10是二进制的1010,也就是
1.01乘上2的3次方,那么对于double类型的数据
它的阶码呢是偏置常数1023 加上这个阶
因此它应该是等于1024加2,也就是
1后面10个0,1,2,3,4,5,6,7,8,9,10, 10个0,然后加2,也就在这一位上的0变成1
所以它的阶码就是等于这个 因此10的double型的表示就是,符号是0
然后阶码部分呢就是10000000010
然后后面的尾数部分的小数部分是01 紧接着后面是若干个0。
这个小数点前面的1是 隐含表示的,不在这个里面显示的表示
这样的话我们最后得到的十六进制数,就是这个01序列对应的十六进制数
那就是0100就是4,然后0000就是0
0010就是2,0100就是4,后面
全是0,这样的话得到64位的一个double型的机器数
写成十六进制形式,就是4024 0000 0000
0000 在这个赋值语句当中,在IA-32这个平台上执行的时候我们说
a呢是一个局部变量,通常是分配在栈区的
实际上在这个栈里面存放的就是这样的64位的值
然后把a作为参数去调用printf的时候
我们要把这个a呢传送到参数区,就是作为printf的参数我们要压栈
那么这个压栈的过程实际上它是由两条指令实现的
第一条指令是fldl,这个很显然我们前面讲过x87
这个指令系统,x87这种浮点数架构 它的指令当中有装入指令
f就表示浮点数,load就是表示装入,这个l表示double类型的,64位的
就是从局部变量区 把一个64位的double型的数据
装入到这个浮点寄存器的栈顶
就是我们讲过x87它用的浮点寄存器是一种栈的形式
栈顶呢是ST(0),所以load的时候都是从某个指定的存储单元开始
比如说装入一个,如果是l的话那就是装入64位double型
装入到ST(0),如果l这个地方改成s,表示single
那么就是装入一个float型的32位的浮点数 装入到ST(0),这是fldl的意思
那么也就是说把这个机器数,64位的double型的机器数 装入到了ST(0)。
然后呢再执行一条fstpl指令 把ST(0)里面的内容再存到
内存,实际也就是存到栈区 这个栈里面是存放参数的地方
所以这个参数呢就被存到了栈里面的一个区域,也就是存到了一个内存单元里面
这样是占了8个内存单元,8个字节 也就是把64位的这个double型的数据
装入到这个80位的寄存器里面 然后再从80位的寄存器里面通过
fstpl这条指令 再把它80位的再转换成60位的double型的再送到参数区
作为printf的参数送过去 所以它整个这个过程实际上是执行的这么两条指令
如果我们把这个double 变成float,就是在
32位机器上面运行的时候,如果这个a是个float 那么最后打印出来的结果是什么样子的呢?如果这个a是float
类型的话,它执行的第一条指令就是把a 作为参数装进来的时候,它首先要在
局部参数区,把这个float型的10 先装入到ST(0)。
这个因为是float型,所以装的是32位的
就是把栈里面局部变量区的一个32位的一个
值转换成80位的一个格式,扩展精度格式 load到ST(0)。
实际上这边的差别就是这边是l这边是s 把一个64位的这样的一个double型的
转换成80位送到ST(0)和把一个32位的等值的
值还是等于10,转换成80位的装到ST(0)。
实际上 这两条指令执行的结果是完全一样的,因为它是一种等值转换
就是把这个64位的数转换成80位的格式,和把
32位的浮点数这个10转换成80位的扩展精度的格式
转换出来的结果是完全一样的因为它们值都是10。
然后再执行fstpl指令 跟它是一样的,再把它放到参数区,其实当a是等于float和double的时候
它最后的结果都是一样的,就是参数区里面放的是完全一样的 这个值,所以最后的结果是一样的。
那么我们来看一下 我们回过头来再来看一下,printf的第一个参数实际上是一个
字符串,送到printf去的这个 参数实际上是这个字符串的首地址
第二个参数呢就是我们刚才看到的从ST(0)那边转换过来送过来的,送到参数区的a
所以我们可以看到参数1是指向字符串的一个指针
参数2呢实际上就是刚才我们看到的那个10
对应的double类型的64位的数据 就是4024
0000 0000 0000 这个就是实际上是最高有效字节
这是高位,这是最低有效字节,所以是直接放进去 然后就是把参数传递好了以后就执行call指令
执行call指令的话实际上是把返回地址放在这儿,然后就去执行printf了 执行printf的时候,那么它就是
根据这个参数的EBP加8这个地方取到第一个参数的首地址,也就是取到这个字符串
然后呢EBP加12这个地方取到的是第二个参数 第二个参数当然就是按%d
去解释的一个数值 按%d去解释的这个数值当然是个32位的数
所以printf就会去取EBP加12那个
地方开始的32位的那个四字节的数拿过来解释 当成十进制数打印出来。
显然打印的是0 因此在IA-32当中,刚才那个程序它打印的这个结果 应该总是0。
实际上打印的是 10所表示的double类型数据的低32位
也就是这边的00000
那如果是64位机器的话,那它的参数的传递方式就完全不一样了
在64位架构里面我们讲过浮点寄存器实际上是用的
XMM寄存器它是128位的
就是不管它的这个扩展精度,实际上到了64位以后
扩展精度还是80位,虽然还是80位,但是我们 给它分配空间的时候现在已经分配的是16字节
在IA-32里面分配的是12字节,因为现在都是 按16字节对齐的,在64位机器里面是按16字节
对齐的,所以它是分配16字节 这个是在64位机器当中浮点数
所占的空间,对于double型还是8个字节 对于这个float型还是4个字节
对于long double型呢就是16个字节了
它的这个存放的寄存器已经不是我们刚才看到的 x87里面的这个ST0
到ST7那个寄存器栈,而是放到 128位的那个SMM这个寄存器
对于刚才的那个例子我们可以看到这个字符串我们用LC1
指向这个字符串的首地址,然后这个里面的10
对应的double型的这个数据是占64位
那么在这个里面对应的是十进制
形式是这样的,其实这个十进制数对应的十六进制数就是它,实际上就是刚才我们看到的
这个10对应的double类型的机器数,因为是小端方式,低地址上面放的是
低位部分,高地址放的是高位部分,所以0先在前面存放
那么高位部分在后面存放,因为这是高地址,这是低地址,所以它是小端方式
这里面的参数传递我们讲过,第一个参数总是在
RDI里面的,第二个参数在RSI对应宽度的寄存器里面的
所以我们可以看到在这个里面 a因为它是double型的,我们前面讲过
在64位架构里面,double型的浮点数 它的寄存器总是用的是XMM
这个实际上是把LC0这个地方的a的值,也就是这边的10
它采用的是相对寻址方式 把这个10对应的机器数送到了
XMM寄存器里面,先送这个
然后呢紧接着把这个参数的首地址,也就是这个string
的首地址在这个里面,把这个首地址送到 EDI,就是第一个参数对应的是RDI对应的
这个宽度的寄存器里面,movl我们前面讲过 它相当于是mov
zlq 也就是把一个32位的数0扩展
变成64位的数,这个和movl是一样的意思
因此在这儿实际上EDI前面的就是 RDI里面高32位实际上是0,因为是0扩展
那么这样的话实际上它就是把字符串的首地址呢送到了这边,也就是这个参数
送到了这边,然后紧接着就去调printf这个函数
所以它这两个参数置好了以后就去调printf这个函数 printf当中里面的格式符是%d
所以呢它要根据ESI 这个32位,因为%d是当成
32位的int型的数据去取的,所以从ESI里面去取打印参数
而ESI里面实际上这里面并没有对它进行复制,因为这边的a
是浮点数,而浮点数被分配到了XMM寄存器里面,而不是
把它送到了ESI寄存器里面,这样的话 printf去取ESI里面的值的时候
ESI里面取的这个值就是一个不确定的 值,看你当时执行这个程序的时候,这个寄存器里面是什么就是什么
本来我们是把这个参数送到ESI或者RSI这个寄存器里面的
但是因为a是浮点数,它送到了这个寄存器里面 所以这个地方就没有衔接好,因为只打印出来的是一个不确定的值
[音乐] [音乐]