在高级语言中,变量和数值操作是很基本的语法,基本所有语言都有,但是其在汇编级别上是怎么实现的?或者拓展了说,在计算机上,变量和数值操作的实现逻辑是怎样的?
主要内容
-
汇编与机器码
首先,我们要知道,在计算机架构中,最重要的两层抽象是指令集和虚拟内存。前者是所有计算机程序的最小逻辑单元,即所有的计算机程序,都会被转换为一连串的计算机指令并运行。而虚拟内存则是计算机对程序可访问内存的一层抽象,因为这层抽象存在,每一个运行在计算机上的程序可以认为内存是一个只有自己和OS在使用超级大的字节数据。而计算机上存储、使用的数据都是二进制数(机器码)的形式,因此,所有的计算机指令都会被转换为对应的二进制数,计算机最终会根据这些不同的二进制数执行相应的计算机指令。但是二进制数是机器的语言,如果我们要直接使用二进制数进行编程,先不提各个机器上的相同指令对应机器码之间的差异性和代码的可读性问题,记忆各个指令对应的二进制数无疑是困难的。而如果我们设计一种人类可以很容易理解、记忆的语言,它与计算机指令有一一对应的关系,而我们只要再创建一个计算机指令编译器,将这种新语言转换对应的机器码,即解决了上述提到的各个问题。
而汇编语言就是上面说的“新语言”,它只是对各个计算机指令二进制数的简单字符抽象,汇编语言的格式与编译器最终转换为的二进制数形式数据并没太大区别。下面是一个简单的代码实例:
【待补充】 -
数据类型
在计算机上没有常规意义的数据类型的概念,即没有Integer,String类型之分。在 汇编语言中,只存在不同长度的二进制数,但是使用二进制数时,指令参数只有以下三种:- 标量,例如
$1
,$22
,当它出现在指令参数时,参数的值就是其对应的二进制数,$
符号只是在汇编的写法习惯,在实际的二进制代码中并不存在$
前缀。 - 寄存器,例如
%rax
,%rsi
,当它出现在指令参数时,表示的值为存储在寄存器中的二进制数。和上面的标量表示一样,汇编的写法上一般会在寄存器名前加上%
做为标识。使用寄存器作为参数时,需要注意,当指令要操作不同长度的二进制数时,我们要使用对应长度的寄存器名。例如,寄存器%rax
的64位、32位、16位和8位的名称分别为%rax
,%eax
,%ax
,%al
,那么当计算机指令操作16位数据时,需要用%ax
标识该寄存器,8位则使用%al
,需要与指令类型严格一致。
- 内存数据,例如
0x100
,(%rdi)
,当指令参数没有$|%
前缀时,那么他就表示的是存放在内存某个位置的二进制数。这个时候,该二进制数的长度只取决于指令类型。()
表示的是一种特殊的内存地址计算规则,计算规则为:100(, %rdi, 2)
,其中%rdi
的值为0x1
,则最终结果为100 + 1 * 2 = 102
,即内存地址为102。
- 标量,例如
访问数据(未完)
算术逻辑操作
思考:
- 在汇编语言中,如何表示高级语言中的变量?
在高级语言中,变量往往就代表着程序的操作对象,可以作为计算结果的存储地点、程序的操作对象,甚至可以表示一段程序的执行过程。但是对于汇编语言来说,不管是可操作的数据,还是计算机指令本身,都只是一连串的二进制数,这些二进制数的具体含义,仅依赖于其在汇编程序中的上下文(指令类型及参数位置)。
但是在汇编语言中并不存在变量的概念,汇编语言的操作对象是各个寄存器和内存数据,那么汇编语言是怎么表示高级语言中变量的语义的呢?
我们先看一段代码,对于C语言函数:
其转换的汇编语言可能是这样的:void concat_char(char a, char b, char *dest){ dest[0] = a; dest[1] = b; }
上面的汇编代码中,movb表示移动一个字节,参数有两个,第一个参数为源地址,第二个参数为目标地址。从上面汇编代码可以看出,对于汇编语言,不同的参数类型只代表着不同的数据大小。concat_char: movb %dil, (%rdx) %dil表示第一个参数所在的寄存器,%rdx表示第三个参数所在寄存器地址,由于第三个参数是指针,因此需要加上()表示取其指向的内存地址 movb %sil, (%rdx, $1, 1) %sil表示第二个参数所在的寄存器,$1表示标量,即$1就表示了数值1,(%rdx, $1, 1)的语义为(%rdx + 1 * 1)指向的内存地址 ret 表示函数结束
即使考虑更复杂的变量类型 ---- JAVA中的对象,其存储于计算机中也只是一连串的二进制数据,对于汇编语言来说也只有使用上的区别,即JVM转换为机器码时,可能会以不同的指令(同一类型指令,但是操作不同大小的数据)去操作对应对象数据的各个一部分(对应着对象实例的不同的字段类型),但是变量本身却只对应着某个寄存器或某个内存地址。
总的来说,我们或许可以这样认为,在汇编语言并不存在变量的概念,但是我们可以通过操作不同数据大小的不同指令,结合寄存器和内存地址来模拟实现变量的效果。