前言
指令集位于软件设计的最底层,对一个想写出好代码的程序员来说,也是应该有一定认识的.对于<深入理解计算机系统>(以下称"本书")来说,理解CPU能做哪些事,是理解进程,虚拟内存,并发的基础.笔者认为稍有遗憾的是,如果本书穿插一些指令集方面的知识,后面的内容理解起来会更顺畅一些.当然人是活的,找寻相关内容,把他们之间的壁垒打通.
引入
计算机应用的层级,从下往上大概是: 硬件电路层(芯片)→硬件抽象层→机器指令→汇编指令→高级语言→语言框架→程序应用.
认识指令集
指令集是一套指令规范,可看作是硬件和软件的分水岭.对于硬件电路(芯片)设计者来说,指令集提出了需求,芯片设计者满足这些需要,并将其符号化为机器指令.对于软件设计者来说,指令集是基础逻辑,他不必关心硬件层是如何用晶体管实现的,只要拿来用就行.
指令集对应着硬件架构,常见的架构有arm,x86-64等.硬件架构不影响在他们基础上封装的编程语言.在arm上运行的C语言和x86上运行的C语言,他们表达的逻辑是相同的,编程语言等于给使用者提供了统一的逻辑接口,就好像烧柴和烧燃气都能把水烧到100℃,而作为使用者只需要拿来泡茶就可以了.他们之间的"差异"是如何解决的呢?这是写编译器的人考虑的问题.
汇编指令是符号化的机器指令.在计算机中,指令和数据一样是二进制数,也就是类似于0x12345678存在.指令和数据区别在于指令有区别于两者的解读和应用方式,可以简单理解成程序计数器(PC)对指令进行管理(笔者没有翻阅过相关书籍,这部分内容属于微电子设计范畴,写代码的可以略过).机器指令难以识读,所以有了汇编指令这样"人性化"的符号出现."指令集"以汇编指令的形式被认识
汇编指令可以用来编写代码,但也很不方便,所以在他的基础上封装出了高级语言---如C/C++,Java,Python等.高级语言和汇编指令之间的对比,是程序员重点关注的内容.简单的从高级语言的表达式和语句出发,看看他们在汇编指令层面是怎样实现的,从中去理解
回顾程序运行以及程序运行中的数据使用
程序做了什么?如何从高级语言和汇编语言的角度看待.
站在高级语言(如C)的角度,"物理"层面上,程序在不断调用函数,使用语句.语句由表达式构成,所以看作程序在调用函数和表达式也是可以的."逻辑"层面上,接收数据,处理数据,并产生数据.
站在汇编语言的角度,不管是调用函数,还是使用表达式,他们都是指令的集合.每条指令由指令码和数据组成.指令集是稍后需要解读的内容,先看数据的使用
进程数据的使用
所有数据,包括输入的,自定义数据等,都是通过寄存器与CPU进行交互的.
1>寄存器负责当前进程数据的调配.
CPU正在执行的进程称为当前进程.每个进程中的数据有形参,局部变量,静态变量(包括局部静态变量和全局变量).本书并没有明确指出静态变量是直接传给寄存器还是以栈帧形式出现.但不管哪种方式,和局部变量相比,多了一个写回的操作.在理解数据传递的时候可以不考虑静态变量.
2>寄存器的分配规则
寄存器只服务于当前进程.所以进入的第一件事是计算形参和局部变量的数量,把之前进程(调用当前进程的进程)内占用的寄存器做好标记,有两种处理方式:一是不动,当前进程避开被标记的寄存器;二是把寄存器数据压入栈帧后,可以使用被占用的寄存器.
1)首先分配形参寄存器.以x86-64为例,有6个参数寄存器,如果形参数量<=6个,则依次放入编号寄存器(见本书P120).如果形参数量超过6个,多余形参放入栈帧(开辟内存空间,并传递数据)
2)为局部变量分配寄存器.本书P173---除了栈指针%rsp以外的寄存器,都可以用来存储局部变量(如果笔者理解有误,就是除了形参寄存器和%rsp以外).有几种情况必须在栈上分配空间存放局部变量--本书P170
1].寄存器数量不足
2].对局部变量使用地址运算符&.这里为什么要对局部变量取地址,不是说局部变量用完后释放内存空间不能用了吗?从栈运行机制来说,在下一个进程到来之前,没有发生数据传送到局部变量所在地址,是可以的.笔者曾经在C++语法应用:从return机制看返回指针,返回引用_c++ 返回引用 返回指针-优快云博客里有过验证.
3]局部变量是数组或结构.
为静态数据分配寄存器和内存空间,笔者认为和局部变量差不多.不过需要多一个回写过程.静态数据是放在堆区,不是放在栈区,不会随着出栈而被释放.
===============================内容分割线==================================
题外话:这部分内容和学习接口函数api也是一样的,只能记忆,不能做什么.
相当于编译器设计人员满足的需求.使用编译器的人,理解什么意思,会用就行了.
当然也不是说写编译器的人就厉害.写应用的人差,不是一个层面.就好像一个是厨师,一个是菜农.做菜的厨师做出来的菜好吃,和种菜的人种出品相很好的菜.但是厨师虽然不会种菜,但会选择种的好的菜一样,写代码的人也要懂一些编译器的知识.
===============================内容分割线==================================
指令集和C语言对比
对于CPU,笔者倾向于看作一个有独立意识的个体(笔者帖子里基本上都是用"他"而不是"它").程序员先了解CPU能做什么,然后以此做基础,将自己的设计思路交给他去完成,这个过程也就是编写代码.
如何知道CPU能干哪些事呢?用高级语言的语句,表达式,已有的api,对照查看CPU指令.使用x86指令集,参照别人的帖:X86和X87汇编指令大全(有注释)_x86指令集手册-优快云博客
注意:x86标准的汇编指令和AT&T标准有一点区别.当有两个操作数时,x86标准指令中,第一个操作数是目的操作数,第二个操作数是源操作数.AT&T标准与之相反.笔者个人感觉,AT&T好读一点.但他们意思都是一样的只是改变了顺序,所以问题也不大.
这部分内容说难也有难度,笔者只是想简单的虚化CPU,写到这里又发现关乎汇编语言和计算机组成原理中的内容,.所以只能尽量描个大概.
CPU基本功能
1>CPU的总线机制:地址总线,数据总线,命令总线.
CPU能识别什么是地址,什么是数据,什么是命令.因为都是二进制数据,这部分是CPU能识别的.
2>CPU识数.从汇编指令move中可以看到立即数是CPU能理解的
3>CPU理解指针机制.
计算机数据都是二进制数,是用地址来访问数据的.CPU懂得直接寻址和间接寻址.给出一个地址,提取地址中的数据,即直接寻址.给出一个地址,先提取地址中的数据如data,在提取以data为地址的数据,即间接寻址.
===============================内容分割线==================================
笔者想到一个问题:在汇编语言层面怎样实现多重指针?
直接寻址和间接寻址机制实现了在高级语言层面的"指针"和"双重指针",那么他如何实现三重以上指针呢?有兴趣的可以做个思考.提示:可以对地址进行"折叠"
编程的时候弄那么多重指针没啥意义.双重指针也够用了,可以把内容抽象成表.
指针的意义在于间接访问一块连续数据.一重指针访问一个连续数据的集合,双重指针访问一个连续数据集合的集合.双重指针的构建:一重指针指向的数据集合中,每一个元素是另一个指针,以此指向另一块连续数据..如果要建立多重指针,那么继续往下延伸即可.---指针核心思路:通过访问地址访问数据
以下例子没有多大意义,只是加深对数据存储的理解.
汇编语言中三重指针如何表示
---------------------------------------------------------------------------------------------------------------------------------
以下内容是第一次编写,有误
//伪代码:建立图示指针关系
leaq 地址0表示数据,地址0 //地址0指向一片空间
leaq 地址1表示数据,地址1 //地址1指向一片空间,地址1等于地址0加8
... //建立表1-0中其他指针
leaq 地址0,表项0地址 //表项0指向表1-0
leaq 某地址,表项1地址 //表项1地址等于表项0地址加8
... //建立表1中其他指针
leaq 表项0地址,指针 //图中汉字"指针"
访问表项地址1中指向数据集合中,地址1指向的数据集合中第2个元素
//伪代码:访问表项1地址→地址1指向集合中第2个元素
add 1,指针 //移动指针到表项1地址处
add 1,(指针) //指针取得表项1,地址1处的值,这个值指向目标元素那张表
leaq (指针),第2个指针 //用第2个指针指向目标元素所在那张表
add 2,第2个指针 //指针指向目标元素
move (第2个指针),%rax //把这个元素装载到寄存器%rax当中
--------------------------------------------------------------------------------------------------------------------------------
用一段C语言的伪代码来对照看
//C语言伪代码
int ***p=a; //假设指向一个名为a的三重表
int b=*(*(*(a+1)+1)+2) //取得第1张表中第1张表的第2个元素
由此可见高级语言表达简练得多.
如果还有其他多重指针,以此类推.每两重指针表示一层.
===============================内容分割线==================================
算术运算指令
加减乘除,四则运算.注意两点:
1> 表示"自加1"或者"自减1"优先使用++和--,因为在汇编语言中都是一步指令:INC和DEC.
2>表示"两个数求和"优先使用a+=b;而不是c=a+b;在汇编语言中也是一步指令.后者多一步move复制指令,并且多占据一个数据(c)的内存空间.当然这是根据实际情况而定的,看是否有需要.
逻辑运算指令
与,或,异或,非,左移(逻辑左移和算术左移),右移(逻辑右移和算数右移)
和高级语言一致.这里的逻辑运算除了"非"是"逻辑非",其余是位运算.C/C++提供了位运算的操作符.注意:没有"按位取反"这个运算符,"按位取反"是由异或实现的.参见C语言基础:掩码运算(位运算)_c 掩码转换位数-优快云博客
程序控制指令
1>无条件转移指令
JMP 无条件转移指令
CALL 过程调用 RET/RETF 过程返回. //相当于高级语言中在进程中调用其他进程
2>条件转移指令
这部分内容包括了比较表达式(>,<.>=,<=,==和!=)和分支结构,相当于
//C语言伪代码
if(x<y) //条件判断
statement; //语句
3>循环控制指令
这部分相当于while
其他指令
程序控制指令里还有 中断指令,处理器控制指令,此外其他指令中还有标志寄存器等内容,可能来自硬件中断.这些内容没被封装进C语言,或者笔者并不了解.略过.
指令集的运用
代码写得是否精炼,是否有冗余现象,可以用一个很简单的方法来判断,看汇编指令的长度.如果为了一个相同需要写了两段代码,编译后短的汇编代码比长的要好.
后记
如果想多了解底层,汇编语言也是可以深入学习,反复研读的.
题外话
理论的建立,是从自然现象中推导的.都是伟大科学家发现的,和普通人关系不大.
写应用级代码,有"建立需要→概念设计→制作工具(找寻工具)→ 编写代码".这一套大致流程.程序员被称为码农,可能是因为设计中绝大部分工作,都已经有人在做,或者比较成熟了.但不管怎么说,在计算机已进入生活中方方面面的时候,他还是在不断往前发展的,所以机会还是会有的,共勉.