汇编1

99 阅读15分钟

一、汇编语言产生的原因 机器只能识别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 11101的原码加-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位这种方法比较浪费寄存器空间(也可能是他们觉得这种方法逼格不够),所以他们想了一个骚一点的方法,两个寄存器的所有位都要使用。 举个粒子

为了表示方便,我用一个516进制数来表示一个20位二进制数,现在要表示0x10ABC这个数;
可以把这个数拆成0x100000x00ABC,
而0x10000往右移一位,相当于它对应的二进制数往右移4位,变成了0x1000,这个时候它就是162进制啦,
并且要让0x00ABC也可以用416进制数来表示,这样就可以用两个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语言层面抽象的东西进行一览无遗地实体化;