计算机的简单编码结束,现在就进入计算机指令级编码(汇编),以及高级编程语言在指令级编码层面的实现(c语言到汇编的过程)。
关于内容,依旧是以我自己的理解。
汇编语言通常是可以用“可执行程序”直接生成的。
而从汇编到源代码(c)的过程,就是一个相对复杂的过程了,就像是用大量的拼图碎片拼出具体图案一样。现在的术语通常把这个过程叫做“逆向工程”。
这里只讨论基于x86-64的指令,英特尔公司的,然后不对其历史发展和过程进行展开,我们只需要知道这些指令和指令集的发展经历了一个很漫长的过程,具有庞大的工程量就行。
开始:摩尔定律,不概述了。
程序编码,编译器将c的代码转换成汇编代码(几乎相当于直接指令了)。
程序计数器(pc),给出下一条指令地址的。
整数寄存器,保存以整数解释的数据;比如整型(int),指针,或者什么其他的。
条件码寄存器,保存条件状态的;比如true,false,或者什么其他。用来实现逻辑运算的。
向量寄存器,保存复杂数据的;最常见的就是浮点数,或者特殊结构体,或者其他的,在cpu和gpu分离的设计之后,gpu中关于这个部件会更多一点吧(可能)。
现在先给出一个源代码和对应汇编代码的简单示例(来自csapp)
1. long mult2(long , long );
2. void multstore(long x ,long y,long *dest){
3. long t=mult2(x,y);
4. *dest=t;
}
上面代码挺简单的,但因为是第一次描述这类代码,我简单解释一下。
第1行是一个函数声明,只给出了函数名和返回值类型以及签名,但是没有给出具体实现。
对了,关于函数的解释,函数就可以理解为一个黑盒子,从外部输入进去某些东西,然后黑盒子输出一些东西,像是工厂里的机器,输入进原料,然后输出生产出来的商品。(在这里就是输入两个long类型的数据,通过mult2这个过程,输出一个long的数据)
第二到第四行是一个带了实现的函数,输入两个long类型的值x和y,加上一个long的指针dest,但是不返回值。通过mult2这个黑盒子处理x和y,输出的返回值赋给t
最后将t赋给解引用的dest指针(dest指针指向的位置)。
然后其汇编代码为:
1.multstore:
2. pushq %rbx
3. movq %rdx,%rbx
4. call mult2
5. movq %rax,(%rbx)
6. popq %rbx
7. ret
上面这一串汇编我简单解释一下,第一行是在指令集的位置加一个标记点,标记点的名字叫multstore,
第二行是用push(压栈)指令将%rbx位置处的值,复制到程序运行时栈顶上。
第三行是用mov(拷贝)指令,将%rdx处的值,复制到%rbx位置处。
第四行是用call(访问)指令,访问mult2程序指令段。
第五行是mov指令,将%rax处的值,复制到“%rbx作为一个寻址数”寻找到的内存的位置。
第六行是用pop(出栈)指令,将栈最顶上的数据,复制到%rbx处。
第七行是一个返回指令,但是没有设置需要操作的内存。
(栈是一种相对简单的数据结构,这里不赘述了,运行时栈就是操作系统内置的保存一些东西的数据结构)
以上汇编指令若仔细梳理逻辑,是可以和上面的简单c代码联系起来的。可以暂时不必全部理解,随着我后面慢慢补充说明。
关于的格式注解,我们的计算机,以64位计算机举例,寄存器设计也是64位的,而以上的指令,操作的大多都是寄存器里的数据,而操作的位数不一定都是满的64位,有时候是一个char,只有8位,有时候是int,只有32位,所以指令的过程需要确定,操作寄存器多少位的值。
相信在我解释上面的push,mov指令的时候,会有人对push后面的q产生疑问吧。
这个q,就是汇编指令对操作多少位的值的一种简单细化,当然,为了规格化设计,所以只有8,16,32和64位。
具体就是:movb,操作1字节的数据,也就是8位(char),movw,操作两字节,也就是16位(short),movl,操作四字节,也就是32位(int,float),最后就是movq,操作8字节(long,double,char*,64位机器的指针都是64位的)。
关于上面的%rax,%rbx,%rdx,这些是cpu设计的时候,设计的存储器中的几个,作为临时的简单存储,现代cpu有16个通用寄存器,从%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp,然后是%r8到%r15.
各种都被分别设计为,保存返回值,被调用者保存,4,3,2,1个参数(按指令序列),被调用者保存,栈指针位置,以及后面的参数和调用者保存空间。
然后关于寻址和指针,有直接使用寄存器里的值的,也有将寄存器里的值作为一个寻址整数(指针),去找到对应位置的值来使用的。
在这边把后面的常用指令给列举一下吧。
一元操作指令:(就是只有一个参数的指令)
push指令(入栈指令),将寄存器里的某个值,放到内存中的程序栈里。直到之后pop指令弹出栈中数据。
pop指令(出栈指令)。
inc D,相当于将D=D+1了,和D++一样的功能。
dec D,相当于D=D-1。
neg D,相当于D=-D,取负
not D,相当于取补,所有位翻转。
二元操作指令:
add A,B,功能为B=A+B。
sub A,B,减,和上面类似
imul A,B,乘,类似
xor A,B,异或,为逻辑运算里的异或,和上面也类似
or A,B,或,逻辑运算,类似
and A,B,与,逻辑运算,和上面类似
加载有效地址。寻址要存在寻址值,并且寻址到合法位置。(就是指针不能未初始化就用,也不能随便指)
div是无符号除法指令,idiv是有符号除法指令,和上面二元运算规则类似。
控制和条件码寄存器,cpu在设计的过程中,设计了用来保存条件状态的一组寄存器,他们描述了最近的算术或逻辑操作的属性。可以检测这些条件来执行条件分支和循环之类的功能(条件码仅可通过set类指令调到寄存器中使用)。
这一块儿我没怎么搞懂。好像一些内置指令和内部的条件码在硬件上设计到一起了。
jmp指令,jmp D意思就是跳转到D标记的位置。类似的还有je,jne,js,jns,因为是条件跳转。
条件跳转是可以用来设计条件分支的,也就是if else的那些。
循环也是条件分支跳转的一种,只需要跳转到之前执行过的指令那里,就是循环了。
比如:(标记点)loop:
dosomething(具体执行的指令)
test指令
jmp loop(这里条件xxx)
switch语句,多重分支跳转,就是设置一个跳转表,跳转表里保存需要跳转的位置,然后test指令确定值
比如:
test指令
jmp loop1
test
jmp loop2
test
jmp loop3
loop1:
dosomething(其他指令)
jmp loop3
loop2:
dosomething
jmp loop3
loop3:
dosomething
虽然写地粗糙了点,但大概就是这么个理念,依旧是条件跳转,只不过在硬件设计上给了部分优化。
运行时栈,后进先出。
控制转移:call指令ret指令,call指令其实和jmp指令类似
数据传送:主要是通过各种各样的复制来完成的。
数组(array)和结构(struct)。
结构可以帮我们自定义自己的数据,用编程语言提供的最小单位的数据。