程序的机器级表示

逻辑右移与算术右移:

逻辑右移:高位补0

算术右移:高位补最高有效位的值

在java中:x>>k:表做逻辑右移,x>>>k:表做算术右移

如果在一个32(w)位机器中,移位操作的位数(k)>32时怎么办?,令k=(k)mod(w)。

  练习题 2.11 在练习题 2.10 中的inplace_swap函数的基础上,你决定写一段代码,实现将一个数 组中的元素头尾两端依次对调。你写出下面这个函数 :

1 void reverse_array(int a[], int cnt) { 2 int first, last; 3 for (first = 0, last = cnt-1; 4 first <= last; 5 first++,last--) 6 inplace_swap(&a[first], &a[last]); 7 }

当你对一个包含元素 1、 2、 3 和 4 的数组使用这个函数时,正如预期的那样,现在数组的元素变成了 4、 3、2 和 1。不过,当你对一个包含元素 1、2、3、4 和 5 的数组使用这个函数时,你会很惊奇地看到得 到数字的元素为 5、4、0、2 和 1。实际上,你会发现这段代码对所有偶数长度的数组都能正确地工作, 但是当数组的长度为奇数时,它就会把中间的元素设置成 0。

http://c-faq.com/expr/xorswapexpr.html

A: first和last的值相等了,为k(对于奇数个元素,2k+1)

B:对于xor_swap这个函数,x和y是不能够指向同一个地址的. 首先就是个未定义行为.

可以参看 http://c-faq.com/expr/xorswapexpr.html 

其次就算不是未定义行为,比如说C语言严格规定了求值得顺序也一样会出现问题,第一步就把x和y变成0了

C:修改为for(first = 0, last = cnt; first < last, first++, last--)就可以了。

 习题2.25考虑下列代码,这段代码试图计算数组a中所有元素的和,其中元素的数量由参数length给出。

1 /* WARNING: This is buggy code */

2 float sum_elements(float a[], unsigned length) {

3 int i;

4 float result = 0;

6 for (i = 0; i <= length-1; i++)

7 result += a[i];

8 return result;

9 }   当参数length等于0时,运行这段代码应该返回0.0。但实际上,运行时会遇到一个存储器错误。 请解释为什么会发生这样的情况,并且说明如何修改代码。

answer:length-1会有溢出,所以改为 < length
有符号数和无符号数比较也有问题,当length特别大的时候,得不到正确的结果。
所以改为:
unsigned i
for (i = 0; i < length; ++i)

****************************************************************

声明:以下的各种字节长度表示都是基于IA32指令集体系结构的。

程序的机器级表示方式:

一个IA32中央处理单元中含有8个存储32值的寄存器。用来存储整数数据和指针

%eax,%ecx,%edx,%ebx,%esi,%edi,%esp(栈指针),%ebp(帧指针)

前六个为通用寄存器。同时eax,ecx,edx的保存和恢复惯例不同于ebx,edi,esi。

操作数类型:

1、 立即数,也就是常数,表示方法:$后面加标准C表示的整数

2、 寄存器,表示某个寄存器的内容,对于双字,可以是八个寄存器的任意一个;对于字,可以是%ax这种,对于字节,可以是%al这种

3、 存储器引用,根据计算出来的地址,访问某个存储器位置。将存储器看作一个很大的数组,用Mb[addr]表示对存储器从地址addr开始的b个字节值的引用。

%eax:0x100

(%eax):0xFF

(其他的变址寻址等不做过多讨论)

***********************************************************************

数据传送指令:传送指令的两个操作数不能都指向存储器位置

mov:将源操作数的值复制到目的操作数中。

源操作数制定的是一个立即数,存储在寄存器或者存储器中。

目的操作数指定一个位置,要么是一个寄存器,要么是一个存储器的地址。

mov类分三种:

1、movb,movw,movl 分别表示:传送字节,传送字,传送双字

2、movsbw,movsbl,movswl,将做了符号扩展的字节传送到字,将做了符号扩展的字节传送到字,将做了符号扩展的字传送到字

3、movzbw,movzbl,movzwl,pushl S,popl D,将做了零扩展的~~~~pushl和popl指令做如下解释:

pushl S(将双字压栈):

R[%esp]<-R[%esp]-4;//把地址减4(栈向地址下降处延伸)

           M[R[%esp]]<-S;//将数据压入

           所以:pushl指令相当于做了sub地址和movl值两条指令。先把栈指针减4,

               然后将新的值写到新的栈顶地址。

               sub $4,%esp  先把%esp内的值减4,

               movl %ebp,(%esp)  把%ebp中的数值放到%esp指向的存储器中的某

                                 个位置

popl D(将双字出栈):

D<-M[R[%esp]];//将数据从栈中取出

           R[%esp]<-R[%esp]+4//把地址加4;

(movs和movz都是把一个较小的数据源复制到一个较大的数据位置)

无论如何,%esp指向的值永远是栈顶。

*****************************************************************

栈在处理过程调用中起极大的作用。在IA32中,程序栈存放在存储器的某个区域。一般将栈倒过来画,栈向下增长,越往下栈地址越小。栈指针%esp保存着栈顶元素的地址。(参见上个部分)

栈和程序代码以及其他形式的程序数据都是存放在同样的存储器中,所以程序用标准的存储器寻址方式访问栈内的任意位置。例如:假设栈顶元素是双字的,那么movl 4(%esp)%edx,会将第二个双字从栈中复制到寄存器%edx

**************************

c语言代码:

int change(int *xp,int y){

    int x=*xp;

    *xp=y;

return x;

}

**********************

汇编代码:

xp at %ebp+8,y at %ebp+12//

movl 8(%ebp),%edx//获取xp,赋给%edx

movl (%edx),%eax//获取*xp,赋给%eax

movl 12(%ebp),%ecx

movl %ecx,(%edx)

***********************

算术操作:

 leal:加载有效地址,

    leal S,D 表示:D<-&S;

但是往往被用来执行简单的算术操作,进行地址计算。

    leal (%eax,%ecx,4)等价于:x+4y

其他指令:INC D   D+1;DEC D-1;

NEG D   -D,NOT D ~D;

ADD 加,sub 减,imul 乘,xor 异或,or 或,and 与,sal 左移,shl 左移,

sar 算术右移,shr 逻辑右移。

***********************************************************

ps:程序存储器包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块(比如malloc库函数分配的)。程序存储器用虚拟地址来寻址。在任意时刻,只认为有限的一份虚拟地址是合法的。虽然IA32的32位地址可以寻址4gb的地址空间,但是通常一个程序只会访问几兆字节。操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址,一条机器指令只执行一个非常基本的操作,简单的算术运算、存储器与寄存器间数据传送,条件分支转移到新的指令地址等,编译器必须产生这些指令的序列,从而实现表达式求值、循环或过程调用及返回等的程序结构。

控制:根据测试结果决定操作执行的顺序。(程序存储器存储机器代码

用jump可以改变一组机器代码指令的执行顺序。

条件码:除了整数寄存器,cpu还维护一组单个位的条件码寄存器,它们描述最近的算术或逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。

除了leal之外的所有的add,shr等指令(上一部分有讲)都会设置条件码,除此之外,还有两类指令也会设置条件码,但是不改变任何寄存器的值(之前的add等指令往往改变目标操作数寄存器中的值)。分别是cmp和test指令

cmp S1,S2  : S1-S2(比较操作)   (与sub行为一样)

test S1,S2 : S1&S2(测试)       (与add行为一样)

它们只用于设置条件码的值。

特殊用法:test %eax,%eax:检查%eax是负数、零、还是正数。

访问条件码:条件码不会被直接读取,用set指令将条件码的值组合后放到一个目的操作数,将这个目的操作数放到之前的8个单字节寄存器元素(%ah那些)中,或者存储一个字节的存储器位置

比较a<b后,将结果放到%eax中。

*************************************

跳转指令及其编码

movl $0,%eax

jmp .L1

movl (%eax),%edx

.L1

    popl %edx

第三行指令会跳过movl指令。在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。

跳转指令的编码方式:

1、 pc相关的:将目标指令的地址与紧跟在跳转指令后面的指令之间的地址做差作为编码。

这些偏移量可以是1,2,或4个字节

2、 给出绝对地址,用四个字节直接指定目标

程序计数器的值是跳转指令后面那条指令的地址,而不是跳转指令本身的地址。将程序计数器里的值加上目标编码(应该就是之气说的偏移量),得到跳转目标地址。

8048757: 72 e7         je xxxxxxx

8048759: c6 05 10 a0   movl $0x1,0x804a010

目标地址:0x8048759-25(0xe7是-25的一个字节的补码形式)

***************************************

条件码+跳转指令:对程序执行进行控制。(控制流)

根据条件码与跳转指令的结合,或者跳转,或者继续执行代码序列中下一条指令。这些指令的名字与set指令是相匹配的。

实现条件分支:

实现循环:先写成goto代码。再写成汇编。do while,while,for。for可以由while转化而来,但是要注意continue的情况,可能造成i无法自增1从而陷入死循环中。

条件传送指令

数据的转移是一种替代策略。先计算一个条件操作的两种结果,然后根据条件是否满足从而选取一个。使用情景受限。匹配现代处理器。

原始的c语言代码:

int absdiff(int x,int y){

    if(x<y)

       return y-x;

    else

       return x-y;

}

使用条件赋值:

int cmovdiff(int x,int y){

    int tval=y-x;

    int rval=x-y;

    int test=x<y;

    if(test) rval=tval;

    return rval;

}

发现在下面程序的汇编代码中没有跳转指令。而当机器运行到条件跳转(也称分支)时,它还不能够确定是否会执行跳转,处理器采用十分精密的分支预测逻辑试图猜测每条跳转指令是否会执行。会存在分支预测处罚。所以最好尽量减少跳转指令。条件传送无需预测测试结果,只是读取源值,检查条件吗,然后要么更新目的寄存器,要么保持不变。

************************************

对于条件和循环的实现不再做多讨论。下面讨论switch语句:

switch语句根据整数索引值进行多重分支。使用跳转表。

跳转表一个数组,表项i是一个代码段的地址,这个代码的实现当开关索引值等于i时应该采取的动作。程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转目标。

先jmp *.L7(,%eax,4),c代码将跳转表声明为一个有7个元素的数组,每个元素都是一个指向代码位置的指针,从而实现间接跳转,每个代码块实现了switch语句的不同分支。

************************************************

过程调用:

包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到两一个部分。在进入时为过程的局部变量分配空间,并在退出时释放这些空间。大多数机器转移控制到过程和从过程中转移出控制这种简单指令。数据传递和局部变量的分配,释放,通过操作程序栈实现

栈帧结构:

机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复。

什么事栈帧?为单个过程分配的那部分栈称为栈帧。

程序执行时,栈指针可以移动,因此大多数信息访问都是相对于帧指针的。(注意返回地址!!)

栈仅仅用来存储一些地址和数据!!不要想着那些指令都在栈里!!!指令是存在于一个程序存储器里面的!!

假设p调用过程q,则q的参数放在p的栈帧中。当p调用q时,p中的返回地址被压入栈中,形成p的栈帧的末尾。返回地址就是当程序从q返回时应该继续执行的地方。

过程q用栈来保存其他不能放在寄存器中的局部变量,原因:

1、  没有足够多的寄存器存放所有局部变量

2、  有些局部变量是数组或结构,必须通过数组或结构引用来访问

3、  要对一个局部变量使用地址操作符&,必须能够为它生成一个地址。

转移控制

call指令,同跳转一样,可以是直接的,也可以是间接的。

call指令的效果:1、将返回地址入栈2、跳转到被调用过程的起始处。

当调用过程返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置(表示程序计数器被赋值为这个返回地址,并跳转到这个地址,执行这个地址对应的指令)

程序计数器:%eip。其中的值表示当前正在执行的指令的地址。

 

寄存器使用惯例:

程序寄存器组是唯一能被所有过程共享的资源。必须保证当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖某个调用者之后要使用的寄存器的值。(我理解为虽然被调用过程有自己的栈帧,但是数据是要放到寄存器里,才能做运算等等操作的)惯例将%eax,%edx和%ecx划分为调用者保存寄存器。

  %ebx,%esi和%edi被划分为调用者保存寄存器。

当P传了一个参数y给Q,如果在Q的操作中将y做了其他的运算。那么在之后P使用y的时候就会出错。这就意味Q必须在覆盖这些寄存器的值之前,先把他们保存到栈中并在返回前恢复它们。用两种方法实现:

1、  调用Q之前,P将y的值保存在自己的栈帧中,当Q返回后,P从自己的栈中取出y

也就是说P保存这个值

2、  将y的值保存在被调用者的寄存器中,然后将这个寄存器的值保存在自己的栈帧中,返回前恢复该值。也就是说Q保存这个值。

关于过程调用详细,见深入理解计算机系统156页(%ebp与%esp一直不断在变)

数组分配及访问:

1、  对于定义 T  A[N],有两个效果:

存储器中分配一个L*N字节的连续区域。L指的是数据类型T的大小(单位用字节表示),用xa来表示起始位置。引入标识符A,A作为指向数组开头的指针,这个指针的值就是xa数组元素i会被放到地址为xa+L*i的地方。

ps:定义char *B[8],指针数组中,元素大小为4个字节(而不是char的一个字节)

假设E是一个int型的数组,计算E[i]: (E的地址放在%edx中,i存放在%ecx中):

使用如下指令:movl ( %edx,%ecx,4),%eax    

会执行计算地址xE+4i,然后读取这个存储器位置的值放到%eax中。

   ps:c语言允许对指针进行运算,计算的值会根据该指针引用的数据类型大小进行伸缩。

   例子:如果A是一个数组,那么*(A+i)表示第i个数组元素(因为A可以表示该数组的起始地址)。

数据对齐:为什么最好做到数据对齐?

假设一个处理器总是从存储器中取8个字节,则地址必须为8的倍数。如果我们能保证所有的double类型数据的地址对齐成8的倍数,那么处理器只对存储器进行一次读操作就可以获取这个double值。否则可能要执行对于存储器的两次读写。虽然无论是否对齐,IA32都能正常工作,但是数据对齐可以提高效率。

*****************************

对于函数指针:

指针也可以指向函数,提供一个很强大的存储和向代码传递引用的功能。

int fun(int x,int *p);//声明一个函数

(int ) (*fp)(int x,int *p);//声明一个函数指针

fp=fun;//将函数fun赋值给这个指针,注意这里不是写成&fun();与数组指针的赋值原理类似

           //函数指针的值是该函数机器代码中第一条指令的地址!

用这个指针来调用这个函数:

int y=1;

fp(3,&y);

ps:注意写法:

int (*fp)(int *x)//表示f是一个指向函数的指针。函数的参数为int*型的,函数返回类型是int

int *fp(int *x)//这句会被解读成 : (int *)fp(int *x),被理解为是一个函数原型,这个函数以int*x

                      //为参数,并且返回类型是int*

个人思考:过程调用中的局部变量等是存放在栈里的,在对这种变量进行操作时,是要放到处理器的寄存器中暂存然后进行操作的。在数据更改后,再写道栈中(也就是更新栈中的数据),当调用结束后,状态信息(%epx和%ebx)弹出栈,栈顶指向返回地址,执行返回地址对应的指令。如果被调用过程的参数是一个指针,那么指针指向的值改变后,内容自然就改变了,指针指向的内容可以被调用者或者被调用者存在自己的栈帧中。

注意:用来保存临时值的寄存器被指定为调用者保存时,函数可以自由覆盖这些值(是因为栈中有么~~);而有些寄存器被指定为被调用者保存寄存器,任何修改这些寄存器的过程都要保存并恢复它们。是指在修改这些寄存器的值之前,要先在栈上保存它们的值。

************************************************************

存储器的越界引用和缓冲区溢出!!(终于看到和csp有关的部分了)

c对于数组的引用不进行任何边界检查,并且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中(这些变量必须保存在栈中,因为我们必须为他们生成地址)。对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或者执行ret指令,就会出现严重错误。

************************************************

在由32位扩展到64位后,寄存器变成了16个,变化如下:

在转为64位后发生了很大的变化。

在过程调用中,通过寄存器翻倍,程序不再需要依赖栈来存储和获取过程信息

需要栈的唯一原因变成:存储返回地址。

如果一个函数需要栈帧,可能原因:

1、  局部变量太多,不能都放在寄存器中

2、  有些局部变量是数组或者结构

3、  函数用取地址操作符来计算一个局部变量的地址(需要求地址的变量要保存在栈中,否则放在寄存器中,没有地址)

4、  函数必须将栈上的某些参数传给另一个函数

5、  在修改一个被调用者的保存寄存器前,函数要保存它的状态。

注意:在IA32中,栈指针随着值的压入与弹出不断前后移动,但在x86-64过程的栈帧通常有固定的位置,在过程开始时通过减小栈指针(寄存器%rcp)来设置(栈向小地址增长),使得可以通过相对于栈指针的偏移量来访问数据,可见,不需要IA32中的帧指针了。(IA32中的帧指针原来是用来固定位置的呀~~)可见,在x86-64中,向栈压数据后不用将栈指针-1,因为栈指针是固定的。并且在调用结束后也不用将慢慢弹出栈中元素(让其指向返回地址对应的栈中位置),可以简单的增加栈指针来释放栈空间。

*****************************************************

原文地址:https://www.cnblogs.com/litian0605/p/5249781.html