一、汇编语言产生的原因 机器只能识别01序列串,以前程序员在写代码时,写的都是0101011101010这些鬼东西,不方便人类理解,美国人希望用英语单词来代替01序列串,为此他们创建了一张表,将可能用到的所有英文符号全部与01序列串对应,也就是说让人类写代码时写的是英文单词,编译的时候,用一个机器把英文单词编译成01序列串,机器就可以拿着01序列串开心地运行了,这个实现英文单词转成01序列串的机器就是汇编器,他们创建的那张表就是ASCII表; 由于需要表示的英文符号(包括abc之类的英文单词在内)大约有128个,一开始可能因为2^7^=128,所以就让一个个的英文符号对应一个个的7位二进制数,但是考虑到扩展性,也就是说保不齐哪天想往这张表里再加几个字符,所以就需要让一个个英文符号与一个个8位二进制数对应,也就是说,表示一个符号,需要一个8位二进制数,这解释了为什么一个字节为8bit;
二、计算机的减法
计算机的加法非常简单,两个01串按二进制加就完事了,但是减法是需要被设计的,下面讲推出减法的心路历程(我也不知道是不是这么回事):
1.首先,用符号位的方式来区分正负数,1~(10)= 0000 0001(2),所以 -1(10)= 1000 0001(2)~;
2.如果就这样让这两个二进制数相加的话,结果等于 1000 0010~(2)~ = -2~(10)~,得不到正确结果;
3.原码是将十进制数转为二进制数得到的码,反码就是将原码的符号位不变,剩余位取反, 比如 -1的反码就是将1000 0001变成 1111 1110,但是如果直接拿反码做减法的话,
1的原码是 0000 0001,
-1的原码是1000 0001,反码是 1111 1110;
1的原码加-1的反码是 1111 1111;
对其结果进行反码得 1000 0000;
结果是-0,产生了逻辑错误(??),所以不能用反码来减(??);
4.补码就是将反码加一,-1的反码1111 1110,补一下变成1111 1111,让它与1的原码0000 0001相加,得到10000 0000,符号位溢出,变成了全0;
三、8086系统的物理地址表示方法
1.8086有三种总线:控制总线、数据总线和地址总线,一个个寄存器可以看做一个小区里的一栋栋楼房。我在我家给女朋友打电话,让她待会来我家的时候别穿绿色衣服 (好吧我没有女朋友),然后在短信里把我家地址发给了她。
我让女朋友来我家并且给女朋友提意见时发的是控制信息,也就是通过控制总线发的,而在短信里发送我家的地址给她,是通过地址总线发的,而她是通过数据总线走来我家的。
2.我的女朋友在离我家3816米的时候不知道还要往前走多少米,于是我准备给她飞鸽传书,但是我家giegie只能带写了3个数字的字条 (写了4个数字的字条太重了它飞不起来) ,于是我往一张纸条里写了一个300,另一张纸条里写了一个816,让两只giegie一起飞,女朋友收到后 (可能) 会知道我的意思,也就是300 * 10 + 816,也就是说你往前走3816米就会到我家啦。
3.这就是8086的物理地址表示方式,CPU地址总线有20条,也就是说地址总线可以一次表示一个20位二进制数,这个20位二进制数有2^20^种可能性,地址总线是拿来表示CPU的物理内存的,也就是说20位二进制数可以表示2^20^个内存单元,一个单元是一个字节,所以这样的地址总线可以表示的CPU大小为2^20^ * 1Byte = 2^10^ * 2^10^ Byte = 1024 * 1KB = 1M,也就是说,这样的20条地址总线最多可以表示1M的CPU,但是8086的CPU只有16位的寄存器,如果说CPU要传出一个20位物理地址的话,起码需要两个寄存器。 可能Intel开发人员觉得一个寄存器用16位,另一个寄存器用4位这种方法比较浪费寄存器空间(也可能是他们觉得这种方法逼格不够),所以他们想了一个骚一点的方法,两个寄存器的所有位都要使用。 举个粒子
为了表示方便,我用一个5位16进制数来表示一个20位二进制数,现在要表示0x10ABC这个数;
可以把这个数拆成0x10000和0x00ABC,
而0x10000往右移一位,相当于它对应的二进制数往右移4位,变成了0x1000,这个时候它就是16位2进制啦,
并且要让0x00ABC也可以用4位16进制数来表示,这样就可以用两个16位寄存器来表示20位地址了,
总结一下,将20位二进制数用某种方式拆分成为两个数,将拆分后的第一个20位二进制数往右移4位,
变成16位二进制数,存入一个16位寄存器中,拆分后的第二个数天然就可以用16位二进制表示,所以也把它存
进另一个16位寄存器中,再把这两个寄存器的数据全部放入地址总线中从CPU中输出,在CPU外部,有一个地址
加法器,可以按照前面提到的算术过程反过来算出真正的物理地址,这样就把CPU里的物理地址搬到了外边儿
看似步骤有些冗余,但这是汇编必须要考虑的东西,也就是一个数字到底是存放在哪里的,以及存放容器够不够放这个数字。
以上便是8086的物理地址表示方式,前面提到的两个寄存器就是段地址寄存器和偏移地址寄存器; 个人感觉很多书很多帖子在8086物理地址的描述上都没有我写的细,在CSDN上搜到的关于这个的帖子感觉都是复制粘贴出来的,要么就是直接说8086用两个寄存器,要么就是抄的王爽老师的书……
四、存储器的演进
1.潘金莲告诉西门庆,你经过我家楼下的时候,如果看到窗台上的蜡烛是燃着的,你就进来,如果蜡烛是熄灭的,你可万万不能来,因为你可能会被武松打死; 潘骚实际上用蜡烛来传递了信息;
2.进入漆黑的房间时,我会下意识的按下开关,把灯打开,灯从不亮的状态转移到了亮的状态,这其实是类似于二进制的,也就是说灯管 + 电 + 人手就可以传递信息了;
3.抽象一点的说,通过消耗某种资源,可以让某个东西在足够长的时间里保持某种状态,让人或机器获取并理解;如果说要让灯管来传递信息的话,实际上需要消耗两种资源---负责开关的人、电能,电已经作为了一种消耗资源,我们也得尽量让电去控制开关;
4.继电器就是这样一个东西,它通电之后可以控制一个小锤锤去捶开关,但这实际上是电能转化为动能来使用;计算机里真正使用的存储器是结型场效应管,它和三极管其实差不太多,但是在某些方面比三极管要优秀,比如它很省电;
翻看了郑益慧教授的模电才勉强看懂,g极为栅极,它与下面的PN半导体因为二氧化硅而绝缘,所以省电啦
(剩下的挖个坑,
期末考试过后再补 )
N - 2、C语言结构体在汇编层面的分析 编写一个很简单的C代码
#include <stdio.h>
struct User{
int age;
char *name;
int money;
long money2;
};
int main() {
struct User user = {17, "Fakeraaaaaaaaaa", 1000, 3000};
struct User *p = &user;
printf("user's address: %p\n",&user);
printf("p->age: %d\n", p->age);
printf("age's address: %p\n" ,&(p->age));
printf("p->name: %s\n", p->name);
printf("name's address: %p\n" ,&(p->name));
printf("p->money: %d\n", p->money);
printf("money's address: %p\n" ,&(p->money));
printf("p->money2: %ld\n", p->money2);
printf("money2's address: %p\n" ,&(p->money2));
return 0;
}
发现结构体的地址就是第一个结构体成员的地址,第二个指针成员的地址是结构体地址+8【4byte(age) + 4byte(填充)】,第三个整型成员是指针成员地址 + 8(注意这是16进制,8 + 8 = 10); 注意:本人x64操作系统,char*占8byte,long int占4byte,int占4byte; 对其在clion中进行反汇编
0x0000000000401550 <+0>: push %rbp
0x0000000000401551 <+1>: mov %rsp,%rbp
0x0000000000401554 <+4>: sub $0x40,%rsp
0x0000000000401558 <+8>: callq 0x401720 <__main>
0x000000000040155d <+13>: movl $0x11,-0x20(%rbp)
0x0000000000401564 <+20>: lea 0x2a95(%rip),%rax # 0x404000
0x000000000040156b <+27>: mov %rax,-0x18(%rbp)
0x000000000040156f <+31>: movl $0x3e8,-0x10(%rbp)
0x0000000000401576 <+38>: movl $0xbb8,-0xc(%rbp)
0x000000000040157d <+45>: lea -0x20(%rbp),%rax
0x0000000000401581 <+49>: mov %rax,-0x8(%rbp)
0x0000000000401585 <+53>: lea -0x20(%rbp),%rax
0x0000000000401589 <+57>: mov %rax,%rdx
0x000000000040158c <+60>: lea 0x2a7d(%rip),%rcx # 0x404010
0x0000000000401593 <+67>: callq 0x402b50 <printf>
0x0000000000401598 <+72>: mov -0x8(%rbp),%rax
0x000000000040159c <+76>: mov (%rax),%eax
0x000000000040159e <+78>: mov %eax,%edx
0x00000000004015a0 <+80>: lea 0x2a7d(%rip),%rcx # 0x404024
0x00000000004015a7 <+87>: callq 0x402b50 <printf>
0x00000000004015ac <+92>: mov -0x8(%rbp),%rax
0x00000000004015b0 <+96>: mov %rax,%rdx
0x00000000004015b3 <+99>: lea 0x2a76(%rip),%rcx # 0x404030
0x00000000004015ba <+106>: callq 0x402b50 <printf>
0x00000000004015bf <+111>: mov -0x8(%rbp),%rax
0x00000000004015c3 <+115>: mov 0x8(%rax),%rax
0x00000000004015c7 <+119>: mov %rax,%rdx
0x00000000004015ca <+122>: lea 0x2a72(%rip),%rcx # 0x404043
0x00000000004015d1 <+129>: callq 0x402b50 <printf>
0x00000000004015d6 <+134>: mov -0x8(%rbp),%rax
0x00000000004015da <+138>: add $0x8,%rax
0x00000000004015de <+142>: mov %rax,%rdx
0x00000000004015e1 <+145>: lea 0x2a68(%rip),%rcx # 0x404050
0x00000000004015e8 <+152>: callq 0x402b50 <printf>
0x00000000004015ed <+157>: mov -0x8(%rbp),%rax
0x00000000004015f1 <+161>: mov 0x10(%rax),%eax
0x00000000004015f4 <+164>: mov %eax,%edx
0x00000000004015f6 <+166>: lea 0x2a67(%rip),%rcx # 0x404064
0x00000000004015fd <+173>: callq 0x402b50 <printf>
0x0000000000401602 <+178>: mov -0x8(%rbp),%rax
0x0000000000401606 <+182>: add $0x10,%rax
0x000000000040160a <+186>: mov %rax,%rdx
0x000000000040160d <+189>: lea 0x2a5e(%rip),%rcx # 0x404072
0x0000000000401614 <+196>: callq 0x402b50 <printf>
0x0000000000401619 <+201>: mov -0x8(%rbp),%rax
0x000000000040161d <+205>: mov 0x14(%rax),%eax
0x0000000000401620 <+208>: mov %eax,%edx
0x0000000000401622 <+210>: lea 0x2a5e(%rip),%rcx # 0x404087
0x0000000000401629 <+217>: callq 0x402b50 <printf>
0x000000000040162e <+222>: mov -0x8(%rbp),%rax
0x0000000000401632 <+226>: add $0x14,%rax
0x0000000000401636 <+230>: mov %rax,%rdx
0x0000000000401639 <+233>: lea 0x2a57(%rip),%rcx # 0x404097
0x0000000000401640 <+240>: callq 0x402b50 <printf>
0x0000000000401645 <+245>: mov $0x0,%eax
=> 0x000000000040164a <+250>: add $0x40,%rsp
0x000000000040164e <+254>: pop %rbp
0x000000000040164f <+255>: retq
下面具体分析(再挖个坑 )
1.首先是开辟空间,其实就是拿rbp和rsp这两个指针搞事儿,rbp是栈底指针,b就是bottom,rsp是栈顶指针;
0x0000000000401550 <+0>: push %rbp 【开辟新的栈帧】
0x0000000000401551 <+1>: mov %rsp,%rbp
0x0000000000401554 <+4>: sub $0x40,%rsp 【创建40byte空间】
从内存来看,也就是rsp指针向上移动40字节,如下图,可看到FE20-FDE0 = 0x40;
当前内存图:
2.下面逐行分析
struct User user = {17, "Fakeraaaaaaaaaa", 1000, 3000};
下面的汇编执行的是上面这条c语句
0x000000000040155d <+13>: movl $0x11,-0x20(%rbp)
0x0000000000401564 <+20>: lea 0x2a95(%rip),%rax
0x000000000040156b <+27>: mov %rax,-0x18(%rbp)
0x000000000040156f <+31>: movl $0x3e8,-0x10(%rbp)【把1000放到栈中】
0x0000000000401576 <+38>: movl $0xbb8,-0xc(%rbp)【把3000放到栈中】
(1)下面这句汇编是把17这个数值放到栈底上方20字节的位置,观察下图,结合rbp的地址,可发现11h,也就是17被放到了61FE00的位置;
0x000000000040155d <+13>: movl $0x11,-0x20(%rbp)
当前内存图:
(2)用rip指针获取一下Fakeraaa这个字符串,通过lea命令取它的地址给rax,
0x0000000000401564 <+20>: lea 0x2a95(%rip),%rax
而0x2a95(%rip)地址可由下图看到,为404000,当执行到这句汇编时,rip所指的应该是下一行汇编代码,也就是说它的值为40156b,而40156b + 2a95 = 40400,刚好就是字符串的地址,AT&T通过小圆括号来取出地址对应的值,再通过lea命令取这个值对应的地址,由于汇编器没有开启优化,所以步骤看起来很臃肿;
(3)把rax中存放的字符串的地址放到栈中,具体位置是栈底指针往上18个字节,也就是 61FE20 - 18 = 61FE08 (注意是16进制哦,18 + 08 = 20); 从下往上看正好是404000,由于这是x64机子,所以不需要前面提到的物理地址计算的骚操作;
0x000000000040156b <+27>: mov %rax,-0x18(%rbp)
(从上图还可以看出,本人的机器采用的数据存储方式是小端法,即存储的数据和地址是逆序,详情见CSAPP的第二章哈~)
当前内存图:
(4)下面就是把那1000的值和3000的值一起放进栈里,过于简单所以直接给出内存图:
至此,初始化结构体那句话就分析完了;
struct User *p = &user;
上面这行C反汇编成下面的汇编代码
0x000000000040157d <+45>: lea -0x20(%rbp),%rax
0x0000000000401581 <+49>: mov %rax,-0x8(%rbp)
非常简单,就是取到17的值,然后通过lea命令取它的地址61FE00,放到rax中,因为第一个结构体成员age的首地址就是结构体的首地址,所以就相当于把结构体的地址放入栈中; 小小总结一下,C语言中创建一个指向该结构体的指针,在汇编中就相当于把结构体的地址放入栈中保存起来,在之后要用到该指针时,直接调用栈中的地址即可;
3.推荐在Clion中调试出这个C程序的AT&T风格汇编代码,再结合IDA的Intel风格汇编代码仔细分析,即可发现在汇编层面,结构体不过是内存存储值和内存存储地址的反复横跳而已;
4.在汇编层面,指针不过是获取地址的命令lea,也就是获取这个值的地址,放入某个寄存器或者栈中,而获取指针所指向的值不过是去到小括号中的地址,拿去里面的值而已,汇编可以对C语言层面抽象的东西进行一览无遗地实体化;