程序的机器级表示1
生成汇编语言

使用以下命令生成汇编文件mstore.s并用vim打开:
linux > gcc -Og -S mstore.c

· 开头的行可以忽略
pushq 这条指令是将寄存器的值压入程序栈进行保存
两种保存器

A称为调用者,B称为被调用者。由于调用了函数B,寄存器rbx在函数b中被修改,逻辑上寄存器rbx的内容在用函数b的前后应该保持一致。有两种解决方法
- 函数A在调用B之前,先保存rbx的内容,执行完函数b之后,再回复rbx原来的内容,称为调用者保存, Caller
- 函数b在使用寄存器rbx之前,先保存rbx的值,b返回之前先恢复rbx原来存储的内容,称为被调用者保存,Callee

不同的寄存器被定义成不同的策略
汇编拆解

1 mulstore三个参数分别保存在寄存器rdi, rsi, rdx中,
2 movq将rdx中的内容复制到rbx中,movq的q表示数据大小
3 call 指令对应函数调用,函数的返回值会保存到rax中
4 下一条指令将rax值送到内存中,内存的地址就存放在rbx中

gcc 数据传送指令四个变种,分别为movb, movw, movl, movq
寄存器发展史
直到现在,8个16为寄存器扩展成了64位,害增加了8个新的寄存器


rax保存目标函数的返回值,rsp保存程序栈结束位置,剩下的用来传递函数参数

操作数指示符
立即数以$符号开头,后跟一个C定义的整数
寄存器带小括号表示内存引用。我们通常将内存抽象成一个字节数组,当需要从内存中存取数据时,需要获得目的数据的起始地址addr, 以及数据长度b。为了简便,会省略下标b
I
m
m
(
r
b
,
r
i
,
s
)
(
有
效
地
址
)
=
I
m
m
(
立
即
数
)
+
R
[
r
b
]
+
R
[
r
i
]
⋅
s
Imm(r_b, r_i, s) (有效地址) =Imm(立即数) + R[r_b] + R[r_i]·s
Imm(rb,ri,s)(有效地址)=Imm(立即数)+R[rb]+R[ri]⋅s
r
b
r_b
rb 基址寄存器
r
i
r_i
ri 变址寄存器
s 比例因子 必须是1,2,4,8

mov指令





当movq指令的原操作数是立即数时,该立即数只能是32位的补码表示,对该数符号位扩展后,将得到的64位数传送到目的位置。

movabsq指令的源操作数可以是任意的64位立即数,单目的操作数只能是寄存器。
例子:

此时寄存器rax内保存的数值如图所示:

8位(bit), 1的原码=0x01,取反=0xfe,再加1=0xff
同理,16位(short)时=0xffff, 32位(int32)时=0xffffffff
(每个数字表示4个二进制数字)


al的长度为8,-1表示为0xff,所以rax的低8位发生了改变


ax的长度是16,所以-1表示为0xffff,rax的低16位发生了改变

eax的长度是32,但当movl的目的操作数是寄存器时,它会把该寄存器的高4字节设置为0
即任何长度的寄存器生成32位值的指令都会把该寄存器的高位部分置为0

源操作数的数位小于目的操作数时,需要对目的操作数剩余的字节进行零扩展或者符号位扩展。
零扩展数据传送指令有5条,指令最后两个字符都是大小指示符,第一个字母表示源操作数的大小,第二个字母表示目的操作数的大小。

符号位扩展传送指令有6条,其中s是sign的缩写,同样最后两个字符也是大小指示符。

数据传送实例

第一条mov指令从内存中读取数值到寄存器,内存地址保存在寄存器rdi中,目的操作数是寄存器rax,由于最后函数exchange需要返回变量x的值,所以直接将变量x放到寄存器rax中。
第二条mov指令将变量y的值写道内存里,变量y存储在寄存器rsi中,内存地址保存至寄存器rdi中,也就是xp指向的内存地址。
程序栈
例如我们要保存寄存器rax内存储的数据0x123,可以使用pushq指令把数据压入栈内,该指令指令执行的过程可以分解为两步:


1 压栈之前,栈顶指针rsp指向栈顶的位置,此处的内存地址0x108, 压栈的第一步就是寄存器rsp的值减8,此时指向内存地址0x100
2 然后将需要保存的数据复制到新的栈顶地址,此时,内存地址0x100处将保存寄存器rax内存储的数据0x123。
pop指令也可以分为两步
1 首先从栈顶的位置读出数据,复制到寄存器rbx。此时,栈顶指针rsp指向的内存地址是0x100
2 然后将栈顶指针加8,pop后栈顶指针rsp指向的内存地址是0x108

pop指令是通过修改栈顶指针所指向的内存地址来实现数据删除的,内存地址0x100内所保存的数据0x123仍然存在,知道下次push操作,此处的数据才会被覆盖。
加载有效地址
leaq S, D ->Load Effective Address
leaq 7(%rdx, %rdx, 4), %rax
表示的含义是把有效地址复制到寄存器rax中
假设寄存器rdx内保存的数值为x, 那么有效地址的值为
7 + %rdx + %rdx *4 = 7 + 5x
对于leaq指令所执行的操作不是去内存地址(5x+7)处读取数据,而是将有效地址(5x + 7)这个值直接写入到目的寄存器rax.
leaq指令还可以用来表示加法和有限的乘法运算。



第一条指令将x + 4y 保存到rax里
第二条指令将3z 保存到rdx里
第三条指令将 x + 4y + 4z 保存到rax里
(由于比例因子的取值只能是1,2,4,8,所以把12进行分解)
一元和二元操作
一元操作数,该操作数既是原操作数,也是目的操作数,操作数可以是寄存器,也可以是内存地址

二元操作数,第一个是源操作数,第二个操作数既是源操作数,也是目的操作数,但不能是立即数

例子:

指令addq 是将rax内的数据与寄存器rcx内的数据相加,二者之和再存储到内存地址0x100处,该指令执行完毕后,内存地址0x100处所存储的数据由0xFF变成0x100

指令subq是将内存地址0x108内的数据减去寄存器rdx内的数据,再存储到内存0x108处,0x108的数据由0xAB变为0xA8

incq指令,将内存地址0x110内存储的数据+1,结果内存地址0x110处的数据由0x13变为0x14
最后一条加法指令,将寄存器rax内的值减去寄存器rdx内的值,最终寄存器rax的值由0x100变为0xFD

(rax)表示rax寄存器的地址,改变的是内存地址的值
rax表示rax寄存器内存储的值,改变的是寄存器的值,不改变内存
移位操作

算术右移补符号位,逻辑右移补0
对于移位量k,可以是一个立即数,或是放在寄存器cl中的数,其他寄存器不行。
salb移位量由寄存器cl低3位决定,
salw移位量由寄存器cl低4位决定
以此类推,双字对应低5位,四字对应低6位


第一条 R[%rdx] + R[%rdx]2 = 3z -> %rax
第二条将rax进行左移4位,等效于乘以2的四次方,24R[%rax] = 48z
其他算数指令操作

条件码




mov指令对寄存器al进行零扩展,最后返回判断结果
另一个例子


sete变成了setl,指令setl是如果a小于b,将寄存器al设置为1, l是less的缩写

判断小于的情况,根据SF(符号标志)和OF溢出标志的异或来判定。
两个有符号数相减,当没有发生溢出时,OF是0,
负数时,SF置为1,正数时,SF不会被置1
负溢出,差值变为正数

正溢出,差值变为负数

通过组合,可以表示各种情况

对于无符号数的比较情况,采用的是进位标志和零标志的组合

跳转指令


条件语句x小于y由指令cmp来实现
跳转指令jl,根据SF和OF的异或结果来判断究竟是顺序执行,还是跳转到L4处执行。跳转指令会根据条件寄存器的某种组合来决定是否进行跳转。

用数据的条件转移来代替控制的条件转移,效率会提高




循环
C语言由三种循环结构,do-while,while,for,汇编语言的循环语句是通过条件测试导跳转的结合来实现的。

cmpq与跳转指令的组合实现了循环操作。

do-while循环是先执行循环体的内容,然后再进行循环测试,while循环是先进行循环测试,根据测试结果判断是否执行循环体内容。
switch 语句

对应的汇编指令


运行时栈
当函数执行所需要的存储空间超出寄存器能够存放的大小时,就会借助栈上的存储空间,我们把这部分存储空间称为函数的栈帧。
返回地址
假设函数P调用函数Q, 函数Q执行完返回函数P

当函数P调用Q时,会把返回地址压入栈中,该地址指明了当Q执行结束返回时,要从函数P的哪个位置继续执行,这个return address的操作是由函数调用call来实现的。
参数传递
如果一个函数的参数数量大于6,超出部分就要通过栈来传递


参数7和参数8是通过栈来传递的。

通过栈来传递参数时,所有的数据大小都是向8的倍数对齐,a4是char,只占一个字节,但分配了8个字节

寄存器名字的使用取决于传递参数的大小
栈上的局部储存


caller的汇编代码

subq指令,栈顶指针减去16,在栈上分配16个字节的空间
根据两条mov指令推断出,arg1和arg2存储在caller的栈帧上,
接下来分别计算arg1 arg2存储的地址
执行call指令调用swap函数
最后通过栈顶指针+16的操作来释放栈帧
例子


可以看出x1占8个字节,x2占4个字节,x3占2个字节,x4占1个字节,局部变量不需要对齐
proc函数需要8个参数,7,8需要通过栈帧传递,参数需要8个字节对齐
寄存器中的局部存储空间

由于函数Q需要rdi来传递参数x,因此函数P需要保存rdi中的x;保存x使用寄存器rbp,rbp定义为调用者保存寄存器,于是有开头的pushq %rbp指令,pushq %rbx是同样的道理。
在函数P返回之前,使用pop指令恢复寄存器rbp和rbx的值
本文深入探讨了程序的机器级表示,包括生成汇编语言、寄存器保存方式、汇编指令解析、寄存器发展历史以及操作数指示符。文章详细介绍了如何使用汇编指令如`pushq`, `movq`, `call`等,并讨论了如何在函数调用中保存和恢复寄存器状态。此外,还涵盖了数据传送、程序栈、加载有效地址、移位操作、条件码、跳转指令、循环、switch语句以及参数传递和局部存储的实现。通过对这些概念的解析,读者可以更好地理解计算机底层的工作原理。"
102616438,5882991,SVM深度解析:最大间隔、对偶问题与核函数,"['机器学习', 'SVM', '支持向量机', '线性分类', '核函数']
195

被折叠的 条评论
为什么被折叠?



