从二进制到HelloWorld
引言
这个内容是从二进制到HelloWorld,介绍二进制代码是如何变成我们的程序的。
我们都知道任何文件,无论是代码、文本还是音频、图片等等,这些文件最终都会储存为由1和0组成的序列。
首先我会介绍这些是如何用二进制表示的。然后,我会介绍计算机的主要组成,这个是为第三部分做准备的。计算机最主要的功能是做计算,因此光表示数据还不够,还得执行指令。第三部分就是介绍二进制序列是怎样变成程序的。
信息的二进制表示
首先是整数,整数可以分成两种,一种是没有符号的,一种是有符号的(不过,Java只支持有符号数)。首先回顾下十进制,它其实是利用一种加权求和的思想在里面,无符号的整数的表示同样是这样,就是我们熟知的二进制。有符号的整数是用补码表示的,它也是一个加权求和,只不过权值不一样,它第一位相当于符号位,它是加的一个负权值。这个就是两种整数的区别。
然后是小数,首先想到的就是沿用整数加权求和的思想,然后小数点左边部分的权重是2的正幂,小数点右边部分的权重自然而然就是2的负幂。不过这种定点表示方法,表示的范围太小,不好用。
浮点数现在都是按照统一的标准进行编码的。浮点数写成类似与科学记数法那样的写法,只不过是指数的底是二进制的。以 float
为例,它是由三部分组成,第一部分是符号位,第二部分是指数部分,第三部分是小数部分。指数部分是无符号数减去127,float
是8位的指数部分,可以表示 0-255,减去127之后就是 -127到128,这样它最大就可以表示2^128左右。小数部分就是一个正常的加权求和。这三个部分通过运算就得到了最终的数值。double
类型的数据也是一样的,只不过第二部分和第三部分占用了更多的位数。注意这三部分和它的表示并不是一一对应的。
浮点数为了让数据的表示连贯起来,又设置了规格化的值、非规格化的值和特殊值三类,这里就仔细不说明了。刚刚看到了,由于此时小数点是可以移动的,因此它表示的范围就很大。
再就是字符,字符很清晰了,就是用的ASCII码表示的。然后我们的汉字是用Unicode表示的 。(更多详情参考资料:)
表示了字符、整数和浮点数,其他的数据类型就都可以表示出来了。就比如说图片类型的数据,一般来说图片是有像素点组成,每个像素的颜色可以通过红绿蓝合成,记录每个像素点三种颜色的深浅值就可以表示出一张图片了。(当然有些是CMYK,对应青、品红、黄、黑四种颜色),然后对不同的压缩算法有不同的拓展名,像无损压缩的PNG格式和有损压缩的JPG格式。
计算机的主要组成
计算机最主要的功能是做计算,因此光表示数据还不够,还得执行指令,下面就进入到了代码的表示了。在介绍机器指令之前,先介绍下计算机的几个硬件。计算机可以简化为CPU、内存和I/O设备,I/O设备就是你的鼠标键盘、硬盘和网络这些。
首先看下内存,程序和数据在工作时是放在内存里的。在软件层面, 可以把内存看作是一个很大的数组,里面装着从硬盘加载进来的代码、堆、共享库还有栈和内核。堆是可以变大小的,在C++中动态分配的指针就放在里面。栈是实现函数调用的,函数调用时,它会把返回的地址和局部变量存放在内存中的栈中,当函数返回的时候就把数据给清掉。栈的空间一般比较小,32位的Windows一个程序栈只有1M,如果写了一个递归,这个函数递归的过深,就会发生栈溢出错误。
再看CPU。CPU里面有程序计数器,简称PC,程序计数器就是一个大小为一个字的存储区域,32位的机器就是32个比特位,64位的机器就是64个比特位,里面放着某个指令的地址,处理器就不断的执行PC指向的指令,然后更新PC,让他指向下一条指令。接下来,看一下寄存器文件,它是CPU内部的储存设备,可以理解为一个临时存放数据的空间。算术/逻辑单元ALU就是做运算的。例如我们要计算 a+b
,就是先把 a
和 b
的值放在寄存器中,然后把这两个值放在ALU中做运算,做完运算的值返回到寄存器或内存里面。
指令集就相当于是一个字典,它给出了处理器能处理的所有指令。每一个指令对应着一个二进制的序列。这是Intel的开发手册,给出了指令的描述和对应的二进制代码。
不同系列的处理器有不同的指令集。目前有两大指令集家族,第一个是复杂指令集(CISC)的x86-64,一个是精简指令集(RISC)的ARM。复杂指令集就相当于C++,写起来比较麻烦但是效率很高,在个人电脑上用的比较多,精简指令集相当于是python,写起来和方便,但是效率比较低,现在手机上一般都用的是Arm的指令集。x86-64的前身是IA32,由Intel公司提出。后来,计算机工业从32位迁移到64位,Intel想使用精简指令集就提出了IA64,不过IA64完全不兼容IA32,这时候AMD搞偷袭,提出兼容IA32的复杂指令集x86-64,后来IA64反响不好就用了AMD的x86-64。
程序的机器级表示
好了,现在就正式介绍代码的二进制表示了。
顺序结构很简单,有直接对应的指令,像赋值就是 mov
指令,做加法就是 add
指令。例如这一段程序
int main(){
int x = 3;
x = x + 1;
}
机器码是
代码语言:javascript代码运行次数:0运行复制 0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 30 sub $0x30,%rsp
8: e8 00 00 00 00 callq d <main+0xd>
d: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
14: b8 00 00 00 00 mov $0x0,%eax
19: 48 83 c4 30 add $0x30,%rsp
1d: 5d pop %rbp
1e: c3 retq
其中最左侧是指令在内存中的位置,程序计数器里面的值就是这个位置。中间部分就是机器码的十六进制表示,最右边就是机器码的文本表示。这段代码的2-5行和8-11行都是函数调用的代码,不用管他,实际上就两句 movl 0x3,-0x4(%rbp) 和 mov 0x0,%eax ,第一句可以看出来编译器对代码进行了优化,第二局是默认的 return 0。
判断结构和循环结构利用了比较 cmp
和跳转 jmp
指令。把上面的例子拓展一下
int main(){
int x = 4;
if (x < 10){
x = x + 1;
}
}
机器码是
代码语言:javascript代码运行次数:0运行复制 0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 30 sub $0x30,%rsp
8: e8 00 00 00 00 callq d <main+0xd>
d: c7 45 fc 04 00 00 00 movl $0x4,-0x4(%rbp)
14: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
18: 7f 04 jg 1e <main+0x1e>
1a: 83 45 fc 01 addl $0x1,-0x4(%rbp)
1e: b8 00 00 00 00 mov $0x0,%eax
23: 48 83 c4 30 add $0x30,%rsp
27: 5d pop %rbp
28: c3 retq
第7行的代码先对 x
和 9
进行了比较,把比较结果存起来,然后第八行上面的结果进行判断,大于 greater
的化就跳转到 1e
,就是 return 0
,小于的化就加 1
循环结构和也是一样的,只不过它会跳转到判断的上方。当 x < 10
时,程序会一遍又一遍循环,到机器码,他会一边又一遍从第7行执行到第10行。
int main(){
int x = 4;
while (x < 10){
x = x + 1;
}
}
机器码是
代码语言:javascript代码运行次数:0运行复制 0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 30 sub $0x30,%rsp
8: e8 00 00 00 00 callq d <main+0xd>
d: c7 45 fc 04 00 00 00 movl $0x4,-0x4(%rbp)
14: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
18: 7f 06 jg 20 <main+0x20>
1a: 83 45 fc 01 addl $0x1,-0x4(%rbp)
1e: eb f4 jmp 14 <main+0x14>
20: b8 00 00 00 00 mov $0x0,%eax
25: 48 83 c4 30 add $0x30,%rsp
29: 5d pop %rbp
2a: c3 retq
有了选择、判断和循环,程序就可以写出来了。
现在,编译器帮助我们生成机器码,我们使用 C
、C++
和 Python
这样的高级语言,虽然不用直接接触到这些机器码,不过这些内容对代码调试、理解栈溢出、代码执行的效率等等都比较关键。
2020@Fu_Qingchen
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2021-01-15 ,如有侵权请联系 cloudcommunity@tencent 删除计算机内存数据程序二进制发布者:admin,转转请注明出处:http://www.yc00.com/web/1748001172a4717441.html
评论列表(0条)