心渐渐失空

导航

不学汇编是学不会操作系统的

  前些日子买了本《深入理解Linux内核》回来读,读了前面两三章,感觉我现在的水平不适合读这本书,里面有很多源代码片段,我应该了解内核的整个架构,再来看细节才对。于是重新找了一本《Orange'S:一个操作系统的实现》,这本书听起来会先讲操作系统的整个体系,挑重点讲,我觉得我先读这本相对广义一些的,再去看Linux内核的细节和源码应该会很好的过度过去,翻开书看了一两页,发现汇编是真的多。我对汇编有了解,但是却不熟,读不懂。于是再次翻开了《深入理解计算机系统》讲汇编语言的那一章,先了解下汇编吧。

  很多时候做事情就是这样,总是有一大堆的事情压在前面,有点像编程里面的函数调用,在执行《Linux内核》这个函数的一开始,需要先调用《操作系统的实现》,于是我调用之,在《操作系统的实现》这个函数的一开始,又要调用《汇编》这个函数。人们往往记不住自己的“函数调用栈”,做后面的事情时忘记了为什么做这件事情,做完事情时忘记了接下来该做什么。要是有个工具能够帮我们理一下就好了,我个人是通过手机便签记录的,但是也常常忘了翻开看。

  下面是学习汇编中写下的笔记,用于以后复习:《正文》

  

一些处理器状态C语言 不可见,而机器码却可以访问或者操作;例如:
    程序计数器PC:%eip:下一条要执行的指令的地址;
    整数寄存器文件:32bit*8个;
    条件码寄存器:保存最近执行的算术或逻辑指令的状态信息;
    一组浮点寄存器:存放浮点数据;
汇编代码不区分有符号无符号、不区分各种类型的指针、不区分整数和指针、不区分数据结构;每一条机器指令只完成一个简单的操作,每一条机器指令和汇编代码一一对应;
利用gcc或者g++等命令可以把C源代码正向编译成机器码;使用objdump -d 可以将机器码反汇编成汇编源代码;
IA32每个指令的长度从1-15字节不等,常用的指令较短。汇编源文件中,以.开头的行都是指导汇编器和链接器的命令;
symbol:
pushl a
movl a, b
addl a, b
popl a
ret
汇编代码风格:ATT和Intel汇编代码格式;gcc -S -masm=intel能够生成Intel风格的汇编代码;
数据格式:
16位数据结构称为字(word);而32位称为双字(double words);64位称为四字(quad words);字符类型在汇编里称为字节;浮点类型称为单精度(4)、双精度(8)、扩展精度(10/12);汇编指令后缀表示操作数的长度:b(字节),w(字),l(双字/双精度)。
 
IA32处理器的通用寄存器:一般为小端字节序
31
23
15
7
%eax
 
%ax    %ah
%al
%ecx
 
%cx    %ch
%cl
%edx
 
%dx   %dh
%dl
%ebx
 
%bx   %dh
%dl
%esi
 
%si
 
%edi
 
%di
 
%esp(栈指针)
 
%sp
 
%ebp(帧指针)
 
%bp
 
在汇编代码中,可以通过这些符号来访问每个通用寄存器的全32位、低16位、和低16位中的高8位和低8位(为了兼容运行在之前处理器上的程序而设,在使用short短整型时也会用到16位的)。最后两个特殊的寄存器只有根据栈管理标准惯例才能修改它们的值;
 
汇编指令的操作数:
立即数:$Imm,Imm是一个C语言表示法的整数,此操作数是一个直接能使用的数,相当于存放在代码段中的数。
寄存器:Ea,    操作数是单独的一个寄存器名字符号,表示对该寄存器的引用,对于读操作是取寄存器里的值,对于写操作是寄存器的位置;R[Ea]
存储器:Imm, (Ea),Imm(Ea),(Ea,Eb),Imm(Ea,Eb),(,Eb,s),Imm(,Eb,s),(Ea,Eb,s),Imm(Ea,Eb,s),内存地址为Imm+Ea+Eb*s处的值,M[Imm+R[Ea]+R[Eb]*s],相当于使用寄存器的值来计算内存地址,对内存地址处值的引用;
访问信息:基本的赋值
将a拷贝到b:
a和b等宽:
mov a,b: movb movw movl
a比较窄,b比较宽,拷贝时不够的位使用符号扩展:
movs a,b:movsbw movsbl movswl
a比较窄,b比较宽,拷贝时不够的位使用0扩展:
movz a,b:movzbw movzbl movzwl
除了拷贝,还会修改%esp的值;
pushl s :将s压入栈中;相当于%esp -= 4; movl s, (%esp);
popl d:将栈顶的双字弹出到d中;相当于movl (%esp), s; %esp += 4;
这些工作可由多条指令完成,但是将常用的工作赋予一条较短的指令,可以减少机器码的总长度。push和pop操作比较常用,就为它们发明了这两个专用指令,机器码长度都是1字节,如果没有push指令,则完成这个动作需要6字节的机器码。
subl $4, %esp使栈指针自减4
addl $4, %esp使栈指针自加4
算术和逻辑操作:基础四则运算
整数算术:
加载有效地址:
leal s, d ;s是一个存储器格式的操作数,d必须是一个寄存器,leal指令将s计算出来的值(是一个地址)拷贝到目的寄存器中,如果将leal换成mov指令则会去RAM的s地址取值后拷贝到目的寄存器;
以下每个指令都有b、w、l版本,分别用来操作不同大小的数;
一元操作:d可以是寄存器或者存储器。
INC d 自加1;
DEC d 自减1;
NEG d 取负;
NOT d 取补;
二元操作:s可以是立即数、寄存器或存储器,d可以是寄存器或存储器。
ADD s, d d+=s;
SUB s, d d-=s;
IMUL s, d d*=s;
XOR s, d d^=s;
OR s, d d|=s;
AND s, d d&=s;
移位:k可以是立即数或者%cl这个寄存器,d可以是寄存器或存储器。
SAL k, d d<<=k;
SHL k, d d<<=k;
SAR k, d d>>=k(算术右移k位,相当于除2);
SHR k, d d逻辑右移k位(补0);
特殊的算术操作:64位(4字)乘除运算,32位与64位转换,IA32处理器支持的64位运算。
imull s  R[%edx]:R[%eax]<-s*R[%eax]  有符号全64位乘法
mull s R[%edx]:R[%eax]<-s*R[%eax]   无符号全64位乘法
cltd  R[%edx]:R[%eax] <- SignExtend(R[%eax])   符号扩展转为4字,高32位使用R[%eax]的符号位填充
idivl s R[%edx]<-R[%edx]:R[%eax] mod s; R[%eax]<-R[%edx]:R[%eax]/s; 有符号64位除法及取余。
divl s R[%edx]<-R[%edx]:R[%eax] mod s; R[%eax]<-R[%edx]:R[%eax]/s; 无符号64位除法及取余。
控制:支持C语言中的选择循环等逻辑操作
CPU除了整数寄存器还维护了条件码寄存器用于控制功能。每个寄存器都只是一个位。
CF:进位标识,最近的操作使最高位产生了进位,可以在指令结束时立刻检查此位判断执行刚刚的指令是否导致溢出。是否溢出(无符号运算中溢出)?
ZF:零标识,最近的操作得出结果为0。是否是0?
SF:符号标识,最近的操作得到的结果为负数。是否是负数?
OF:溢出标识,最近的操作导致一个补码溢出-正溢出或者负溢出。是否溢出(有符号运算中溢出)?
设置条件码:
算术和逻辑操作小节中的leal指令的执行不会改变任何条件码。算术和逻辑操作小节中的一元、二元、移位操作会设置条件码,各个指令会设置的位不同。
控制指令只设置条件码而不操作其他寄存器:
CMP s2, s1;根据操作数的宽有三个版本 cmpb,cmpw,cmpl;根据操作数差值(s1-s2)设置条件码,类似SUB指令,只是SUB指令还会设置目标寄存器。ATT格式的汇编代码中两个操作数位置相反。
TEST s2, s1;根据操作数的宽有三个版本 testb,testw,testl;计算s1&s2来设置条件码,类似于AND,但是TEST只设置条件码。常用法:testl %eax, %eax;用来判断R[%eax]是0、负数、还是正数;也可以测试R[%eax]中的几个位:testl %eax, $0x0000ff00;
访问条件码:将条件码的值移到D参数中,注意D只有8位宽,可以是8位的寄存器(为了得到32位的结果,应另将此寄存器的高24位设置为0),也可以是RAM,以下指令将D设置为0或者1。以下大部分指令有同义名。^指异或操作,而不是次方运算。
//判断是否是0
sete d;setz;D <- ZF
setne d;setnz;D <- ~ZF
//判断是否是负数
sets d;D <- SF
setns d;D <- ~SF
//有符号比较结果
setg d;setnle;D <- ~(SF^OF)&~ZF
setge d;setnl;D <- ~(SF^OF)
setl d;setnge;D <- SF^OF
setle d;setng;D <- (SF^OF) | ZF
//无符号比较结果
seta d;setnbe;D <- ~CF & ~ZF 
setae d;setnb;D <- ~CF
setb d;setnae;D <- CF
setbe d;setna;D <- CF | ZF
C语言使用数据类型来区分有符号和无符号数据类型,但是汇编没有数据类型,只有内存宽度,所以对于有符号操作和无符号操作,需要使用两套指令,根据指令不同,就能知道该指令的操作数是有符号还是无符号,有的操作有符号和无符号使用相同的指令。
 
无条件跳转:jmp指令
jmp .L1  汇编器标记作为目标,直接跳转。跳转到.L1标记处开始执行
jmp *o  o为一个操作数格式的操作数。如下:
jmp *%eax  间接跳转,寄存器的值作为目标。寄存器中的值是多少,就跳转到多少地址处接着执行。
jmp *(%eax)  间接跳转,存储器的值作为目标。寄存器中的值作为地址,读到该地址处的值,跳转到该值指示的地址处接着执行。
有条件跳转:条件为真就跳转,条件为假就继续顺序执行,有的指令有同名的指令(同名指令是因为意义不同,但是效果相同,所以就相当于同名了)
je  .L;或jz;跳转条件:ZF
jne  .L;或jnz;跳转条件:~ZF
js  .L;            跳转条件:SF
jns  .L;           跳转条件:~SF
jg  .L;或jnle;跳转条件:ZF
jge  .L;或jnl;跳转条件:ZF
jl  .L;或jnge;跳转条件:ZF
jle  .L;或jng;跳转条件:ZF
ja  .L;或jnbe;跳转条件:ZF
jae  .L;或jnb;跳转条件:ZF
jb  .L;或jnae;跳转条件:ZF
jbe  .L;或jna;跳转条件:ZF
反汇编生成的跳转代码没有.L标记,而是直接指明跳到一个数,跳转的目标行号=下一行行号+跳转指令参数,向前跳转时,这个参数为负数。参数以补码的方式解释。
使用无条件跳转能实现C语言中的goto语句,使用有条件跳转配合无条件跳转可以实现C语言中的选择、循环等逻辑。
 
从1995年的PentiumPro开始,IA32处理器都拥有条件传送指令,可以代替条件跳转指令,但是性能未必是提升的,在GCC中只有少数情况(处理器重叠执行多条指令的不同部分不容易出错时(条件值容易预测时))会使用它们做替代。
条件传送指令:当条件码为真时将s拷贝到r,当条件码为假时不拷贝。s可以是寄存器值或者存储器值,r是寄存器值,s和r长度相同,每条命令可以接受双字节或者四字节长的参数(根据寄存器r的名字自动推导参数长度)。有的命令有同义名。
cmove s, r;或cmovz;跳转条件:ZF
cmovne s, r;或cmovnz;~ZF
 
cmovs s, r;跳转条件:SF
cmovns s, r;跳转条件:~SF
 
cmovg s, r;或cmovnle;跳转条件:~(SF^OF)&~ZF
cmovge s, r;或cmovnl;跳转条件:~(SF^OF)
cmovl s, r;或cmovnge;跳转条件:SF^OF
cmovle s, r;或cmovng;跳转条件:(SF^OF) | ZF
 
cmova s, r;或cmovnbe;跳转条件:~CF & ~ZF
cmovae s, r;或cmovnb;跳转条件:~CF
cmovb s, r;或cmovnae;跳转条件:CF
cmovbe s, r;或cmovna;跳转条件:CF | ZF
 
 
过程:函数调用。
IA32程序用程序栈来支持过程调用,机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复、以及本地存储。
寄存器%ebp为帧指针,%esp为栈指针,大多数信息的访问都是相对于帧指针的。帧指针标记函数栈帧的开始处,栈指针标记当前栈顶地址。
当函数P调用函数Q时,先将Q的参数压入P的栈帧中,顺序为逆序压入(及函数最后一个参数最先压入),然后将Q的返回地址(执行完Q过程后即将执行的代码段首地址)压入P的栈帧中。然后开始Q的栈帧,Q的栈帧开始首先把%ebp的值压入,也就是把P的帧指针值备份。然后%ebp的值更新为当前栈指针%esp的值。然后开始压入其他需要备份的寄存器值(下图中没有画出)、Q函数的本地变量(局部变量)、临时变量等,直到函数返回时弹出当前栈或者调用其他过程时压入新的函数栈。
分配栈对象时,只是执行push指令减小%esp的值,并不会初始化新压入空间的值,这就是未初始化局部变量初始值不确定的原因(当然堆空间对象分配时初始值也是不确定的)。
https://blog.csdn.net/wangyezi19930928/article/details/16921927
call命令:过程调用,效果:将返回地址(紧跟当前call命令的后面一条命令,过程调用结束后返回此处)入栈,并跳转(修改PC值,PC值存放在%eip寄存器)到调用过程起始处。
call  .L;直接调用;调用从标记处开始的汇编代码(函数)。
call  *o;间接调用;o为一个操作数格式的操作数。跳转到寄存器值或者内存值指定的地址处开始函数。
leave;为返回准备栈;相当于以下指令组合:movl %ebp, %esp;popl %ebp;也就是将当前整个函数栈帧弹出的效果。
ret;从过程调用中返回;从栈中弹出返回地址,并挑转(修改%eip寄存器值)到这个地址处。
 
寄存器使用惯例:IA32惯例:
调用者保存寄存器:%eax、%edx、%ecx,被调过程可以覆盖使用。
被调用者保存寄存器:%ebx、%esi、%edi,被调过程使用时需要先备份到栈中,过程返回前需要回复这些寄存器的值。
 
GCC特点:每个函数在调用下一个函数时,在进入下一个函数栈之前本函数的栈大小都是16字节的整数倍,如果不是,则会加入一些无用空间填充,无用空间加在为下一个函数分配实参空间之前。每个函数栈的开始地址都是16字节对齐的。
 
以上是IA32处理器的汇编指令说明,后续x86处理器推出了x86_64处理器,将32位通用寄存器等做了扩展,同时也兼容32位指令,新增加了一些64位指令。
未完......(编译器中汇编指令是如何与C语言中各种语法对应上的,64位处理器汇编扩展)这部分暂时不看了,等以后有时间再接着看完,先学我的操作系统内核去了~~~
 
 
 
 
 

posted on 2018-07-27 11:42  心渐渐失空  阅读(987)  评论(0编辑  收藏  举报