前言
以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定
引入
本书第三章:程序的机器级表示内容的理解,这一章内容以汇编语言为主.汇编语言偏底层,用于系统级别的程序编写.如果不是做系统的,可以不用深入学习.理解汇编语言的关键是理解gcc指令,数据,寄存器,地址等概念.这些概念在C语言中也是很重要的,而且从汇编语言的角度会发现更多细节.
这章可以作为学习C语言的辅助,明白C语言的运行机制.其中重点关注能优化代码的部分.
本帖内容为3.4节访问信息
一.寄存器
本书P119讲了寄存器的演化历史,和指令集历史同步.
1>寄存器是干什么的?
大致推导代码执行过程:源代码经编译汇编链接后,成为可执行文件并加载到内存中.可执行文件是十六进制组成的机器码,实际上是一条条的指令,交给CPU执行.
每条指令有指令码和操作数.例如
movl $0x4050,%eax
指令码由PC(程序计数器Program Counter)管理,存放着指令地址,执行完一条指令后,自动指向下一条指令的地址.CPU执行相应的指令,作为软件开发者可以不管CPU是怎么完成指令的,因为指令码提供了机器活动的抽象,比如movl这条指令,程序员调用后,将立即数0x4050送到寄存器%eax中.
指令码的实现由芯片开发人员完成,我们可以设想一下:把一个十六进制数0x4050,送到%eax中,按照二进制0B0100 0000 0101 0000,CPU接到这条指令,把%eax(32位寄存器)中对应的值等于1的位打开,对应值等于0的位关闭.------------这段属于超纲内容,知道他的结果就行了.
CPU有个特点:只能处理来自寄存器的数据,指令中的操作数必须经寄存器传给CPU.寄存器的作用是把内存的数据调入交给CPU处理;再将CPU处理好的数据交给内存.简而言之,寄存器是用来中转数据的.
2>认识寄存器
本书P120画出了x86-64的整数寄存器图,如果想使用汇编语言,这张图挺重要,最好能熟悉.
简单认识一下寄存器,有64位,32位,16位,8位之分,分别对应四字,双字,字和字节.各自特点如下:
64位寄存器特点:以"r"开头,非"d"结尾
32位寄存器特点:以"e"开头或者"d"结尾
8位寄存器特点:以"l"或者"b"结尾
其余是16位寄存器
在P120倒数第二段,有关于寄存器指令的特别规则:当这些指令以寄存器为目标时,生成4字节数的指令会把高位4个字节置0.
说明:指令有以下2个特点:
1)指令有单个操作数或者两个操作数(3个以上的暂时还没见过)
2)指令末尾要加上表示操作数字节长度的标识(字节加b,字加w,双字加l,四字加q)
当第2个操作数为寄存器,且指令以"l"结尾时,高位4个字节将置0.
3>每个寄存器的作用
本书P120最后一段,强调了栈指针%rsp,知名运行时栈的结束位置.P121第一段:有一组标准的编程规范控制着如何使用寄存器来管理栈、传递函数参数、从函数的返回值,以及存储局部和临时数据.(黑体字是原话).这段话表明了寄存器有使用规则,具体怎样使用在3.7节
二.操作数指示符
操作数的类型有三种:立即数,直接寻址(寄存器寻址和内存寻址),间接寻址(内存地址).为了方便下面的理解:操作数其实是两个类型,立即数和地址
操作数的抽象模型是这样的:
以下是直接寻址和间接寻址的示意图
直接寻址:当标识是寄存器名称或者内存时,实际上表示的是他的数值
间接寻址:当标识用()括起来的寄存器名称或者内存时,表示以他的数值为地址内的数值.
由此引出一个非常重要!非常重要的概念
内存中的数据都是以地址形式出现(立即数除外)
从这个结论对前面所学内容做个回顾和对比:
1.首先是指针的重要性,在C语言等高级语言中,有"变量"的存在.变量是为了方便编写和使用程序的人理解而设置的.CPU不认识变量,他是通过数据地址来运算的.所以变量是地址的代名词.
2.立即数的用途不多,在高级语言中,一般使用枚举来表示常量,避开硬编码,侧面加强了这个概念,突出地址的重要性.
3.地址传递数据这一机制,由接收方直接寻址或间接寻址来表达传的是值或者指针,非常好用.这句话很拗口,用代码来说明
//以下标记为代码段1
movew $0x1040,0xff00 //把0x1040写入地址0xff00
movew $0x1000,0x1040 //把0x1000写入地址0x1040
movew 0xff00,%ax //把0xff00的值0x1040传给寄存器%ax
movew %ax,0x40 //把寄存器%ax里的值传给地址0x40,此时0x40的值等于0x1040
movew (%ax),%rdx //把寄存器%ax值表示的地址的值传给%rdx,此时%rdx值等于0x1000
由%ax接收数据后,%ax表示接收到的值,由(%ax)表示值的地址里的值.因此可以忽略数据类型是"变量"还是"指针".这一点细微的差别,能感觉得出比起高级语言中需声明指针,再间接访问数据这种写法来得更好.-------第3点属于思路发散,实际意义没多少,个人感觉创造出"间接寻址"这个概念的人很厉害
关于操作数指示符的其他内容,可以参看本书P121和P122,并做一做练习题.
三.数据传送指令
本书P122说了数据传送指令是最频繁使用的指令.回到编程的根本目的---用数据来表示结果.传送指令相当于直接写入结果. 运行程序中各种参数传递,结果回传等,都会用到move指令,所以使用很多.
笔者把数据传送指令的规则做个归纳和说明
1>源操作数可以是立即数,寄存器或者内存地址;目的操作数必须是地址
和上面操作数讲的一样,源操作数用寄存器名称或者内存地址,表示他的数值.目的操作数必须是地址,不能是立即数.以下写法错误
//错误写法
movew 0x40 $0xff00 //目的操作数是立即数
2>源操作数和目的操作数不能都是地址,如果把一个地址的数据传到另一个地址中,需要经过寄存器,两步转换.
movew 0xff00,%ax //把0xff00的值传给寄存器%ax
movew %ax,0x40 //把寄存器%ax里的值传给地址0x40
这样写是错的
//以下为错误写法
movew 0xff00,0x40 //不能将一个内存的值直接传给另一个内存地址
//以下是正确写法
movew $0x40 0xff00 //立即数写入内存
3>寄存器部分的大小必须与指令最后一个字符(b,w,l,q)指定大小匹配(本书P123原话).move指令对于内存地址没有限制
moveb %ax,0xff00 //错误,b表示字节,%ax表示字,大小不匹配
moveb %al,%bl //正确,寄存器大小匹配
4>特殊的movl指令:movl指令用于替代movzlq指令.和寄存器特殊运算规则一样,当以寄存器为目的操作数,指令使用movl时,它会将该寄存器的高位4字节设置为0.
movl %eax,%rdx //错误,%rdx高4字节位被置0(不可预料后果)
5>某些寄存器不能被作为目的操作数
movb $0xf,(%ebx) //错误,被调用者保留位,不能被写入
====================================内容分割线============================
笔者在看完了move指令的用法,做了相关习题后,确实有些"嫌"汇编语言.他好不好学,好不好用笔者不知道,但就只是一个指令,就有这么多规则要遵守,在写代码段1的时候,还很小心地对地址进行了计算(每个数据占8个字节,因此地址末尾应该是0x00,0x40,0x80,0xc0)
回到理解汇编语言的初衷,加深对C语言的认识,以及优化代码的基础上去学汇编语言是很好的.但如果可以用高级语言替代,又没有那么大的兴趣去研究的话,相关内容可以考虑不看.
====================================内容分割线============================
P125的3.4.3数据传送示例,有一个地方需要注意,在程序传参的过程中,实际上还发生了一些代码调用.具体要结合程序中上面的代码.
//将xp地址传给寄存器%rdi(第一个参数),这里xp是形参,实际上是实参地址
//将y地址传给寄存器%rsi(第二个参数),这里y是形参,实际上是实参地址
//xp in %rdi的伪代码实现,y in %rsi的伪代码实现
moveq xp地址 %rdi
moveq y地址 %rsi
四.程序栈的初识
本书P127有几句重要的内容(黑体字是原话)
1>程序栈存放在内存某个区域
2>栈向下增长
3>栈指针%rsp保存着栈顶元素的地址
这里程序栈内容并没有讲栈内内容如何与寄存器产生数据交互.
重点:栈指针始终指向栈顶,保存着栈顶元素的地址.每当数据压入栈中,栈顶地址值加8(8个字节),每当数据弹出栈,栈顶地址值减8,汇编代码如下
subq $8,%rsp //数据压栈,栈向下增长,栈顶元素地址减8
addq $8,%rsp //数据出栈,栈向上增长,栈顶元素地址加8
栈指针%rsp的作用,就和指向链表首个元素的指针一样(可以访问链表中任意一个元素),获得栈指针可以访问栈内任意元素(每次移动8个字节.),并将其传给寄存器,以使CPU获得数据后执行指令.
小结
3.4节的内容对理解程序的执行过程还是非常重要的