零、本章概要
指令寻址:解决的是PC+"1"的问题
数据寻址:使用寄存器/内存/结合
基址寻址:用于多道程序的并发执行
直接寻址:call 0x12345678
变址寻址:esi edi用于循环,因为使用直接寻址需要一堆地址很麻烦,速度也慢
相对寻址:地址针对的是相对于PC的位置,在代码位置发生改变的情况下用于实现跳转
PSW就是EFLAGS
x86汇编:
使用cmp+jnz(等)实现跳转指令;cmp会修改PSW的内容,jnz指令执行的适合会根据psw来判断需不需要跳转
循环语句:for(int i=0;i<10;i++){s+=arr[i];}:i++后面紧跟cmp
函数调用底层实现:
堆栈平衡:函数调用前后esp和ebp的位置不变,一个线程内的所有函数共用一个堆栈
x86的push和pop默认是按照4字节压栈/出栈的
esp指向的是栈顶的位置(有元素)
push eax:①先sub esp,4 ②再mov [esp],eax
pop eax:①先mov eax,[esp] ②再add esp,4
call eax:①push eip ②jmp eax
ret:pop eip(默认是ret 4)
真实的函数堆栈以及函数调用(见C语言函数调用的汇编视角)
CICS和RISC指令集的对比
一、指令格式
操作码:做什么 地址码:对谁做
当然停机指令特殊,不需要地址码,就是执行一个特殊的命令而已
Intel的CPU使用的是x86架构,x86架构的指令集和AMD的指令集是不兼容的,不能跨不同指令系统的机器执行,苹果公司宣布要抛弃Intel的CPU,转而开发自己的基于ARM架构的CPU,这样手机和电脑就能通用了
了解即可:
堆栈型计算机通过零地址指令来进行算术运算:操作数是隐藏在栈里面的,而不会显示地在栈中指明,扫描到操作符就相当于扫描到了一个零地址指令,会把操作数计算/弹出堆栈
所有指令访存次数都要加上取指令的那一次
注意:指令字长的长度是可变的(但是得是比如1B的整数倍),机器字长和存储字长都是不可变的
如果指令字长超过机器字长的话就需要多次的取指令操作,CPU可以根据操作码的类型来判断
我们所有的指令都是遵循操作码+地址码这种格式,只是地址码有可能为0,只有操作码这种情况出现,表示仅执行一次特殊的操作
二、扩展操作码指令格式
操作码全1用于区分是几地址指令
这里重点讨论的是n位操作码,能表示2^n种对于3/2/1地址的操作
三地址:15条0000-1110 A1 A2 A3
二地址(12条):1111 0000-1011 A1 A2
一地址(62条):1111 11 000000-111101 A1
零地址(32条):1111 11 11111 00000-11111
cpu分析指令的时候就根据前几位是1来判断他是几地址指令
三、指令寻址
PC+“1”
指令系统采用定长指令字结构: 定长指令+可变长操作码
因为每一条指令的长度是确定的,所以+"1"即可处理下一条指令
指令执行的时候先读入一个字
这里我想给大家重点讲解一下!!!!!!!!!!!!!!!!!!!!!!!
你CPU不是要从内存当中取指令吗?因为CPU一次最多只能同时处理一个字的数据,你就直接去指令所在的内存单元(在PC里存放地址)去取出一个字的内容,但是啊这一个字不是都有效啊!我CPU处理第一个字节的时候发现是55,后面直接扔掉就是了,然后根据55我就能知道这条指令的长度了,也就能知道PC应该加几了!然后可以继续去取下一条地址了!!!!!!!!
四、数据寻址
根据寻址特征和形式地址可以确定操作数的真实地址(EA)
了解即可,不用看:
立即寻址:立即数#010
直接寻址:call 0x12345678
间接寻址:lea [0x123456],地址放在某个内存单元里面
寄存器寻址:push ebp;mov eax,ecx
寄存器间接寻址:push [eax+4]
基址寻址就是用于多道程序并发执行的
直接寻址的bug:要用一堆地址
如何解决? esi edi就是变址寄存器,用于实现循环:rep stosw
直接寻址:地址是几就是几,你位置移动也是跳到2那里
(如何解决这个问题?)
你移动代码对顺序执行的指令没啥影响,但是跳转指令难绷啊😂😂😂
所以只要修改跳转指令的解释方式:因为PC会自动+1,所以使用-4(补码)来指示当前的CPU情况下应该跳转到哪里去执行
转移指令使用的都是相对寻址!!!
基址寻址:整个代码在内存当中的浮动;相对寻址:一段代码在程序内部的浮动
比较(相减)和跳转是分开的
PSW就是我们熟悉的ELAPGS
五、x86汇编语言基础
指令格式是什么样的取决于你的CPU,硬编码就是指令集里面的指令
55就是opcode,66就是前缀,CPU根据值来判断
55为啥是push ebp而不是push bp和当前CPU的模式有关,x86的保护模式默认是32位
前缀指令分成4个组,每个组有n个最多出现一个
定长指令:opcode确定了,指令的长度就确定了
变长指令:即使opcode确定了,指令的长度依然无法确定
opcode决定了有没有ModR/M,也决定了是不是定长指令
比如opcode是88那它后面一定根一个opcode
地址偏移和立即数
比如这种考题:
为啥x86汇编语言不允许两个操作数都同时来自主存?
1、指令长度太长了
2、访问2次主存太慢了吧。。。
x86:intel的8086/80286 80386
一条指令由操作码和若干地址码组成
进行除法运算的时候要对被除数进行位扩展:32->64(edx:eax) ,商存入eax,余数存入edx
intel格式(Windows)&AT&T(Linux&Unix)格式(汇编格式)
六、C语言选择语句的汇编实现
在x86当中IP就是PC
和前面提到的一样,我们这样写汇编代码,但是其实最后变成机器码的时候他会给我们翻译成与PC有关的指令,这样代码段即使移动位置,当PC指向这条语句的时候也能够跳转到正确的地址(使用PC+一个补码)
使用标号可以更方便程序员去编写汇编代码,最后编译器要做的就是把这个标号改成对于的地址
条件转移指令通常与cmp指令配合食用🤣🤣🤣
switch语句:离散情况不生成大表和if...else效率一样,连续的时候生成大表,先比较数值-1超过第二大的数就直接跳到default,否则直接根据edx*4+func(函数首地址) 来去查大表,直接得到跳转地址而不用像if..else那样进行多次的比较!!!
只有特定的指令(比如运算指令,和ALU有关,本质上不就是加法器那些产生的符号位吗?)才会修改PSW寄存器里面的标志位,cmp指令本质上就是做了一个减法(但是结果不保存,只改变符号位),产生了符号位,然后条件转移指令再根据减法所产生的标志位(去PSW寄存器中获取)来决定要不要进行跳转!!!
六、C语言循环语句的汇编实现
i++后面紧跟的就是cmp,一旦越界就跳出循环
for:①先初始化,第一次直接判断是否跳出循环
②循环主体&i++③cmp i ,cnt(没结束就往上跳是for循环的一大重要特征!)
七、C语言函数调用的汇编视角
x86的push和pop默认是按照4字节压栈/出栈的
esp指向的是栈顶的位置(有元素)
push eax:①先sub esp,4 ②再mov [esp],eax
pop eax:①先mov eax,[esp] ②再add esp,4
call eax:①push eip ②jmp eax
ret:pop eip(默认是ret 4)
真实的堆栈以及函数调用:
所谓堆栈平衡就是函数调用前和调用后esp和ebp的位置保持不变!
试试看能不能画到堆栈平衡?
外平栈(默认采用的调用约定):cdcall(右左,外平栈:add esp,8)
内平栈:stdcall(右左,内平栈:ret 8(相当于add esp,8))
注意:一个线程里面的所有函数都使用同一个堆栈
int main(){
fun(2,3); push 3 push 2 call fun(push eip + jmp fun)
}
fun:
八、CICS和RISC指令集的对比
你RISC只能通过寄存器来访存,寄存器可不得比CICS多吗?