从二进制到HelloWorld

引言这个内容是从二进制到HelloWorld,介绍二进制代码是如何变成我们的程序的。我们都知道任何文件,无论是代码、文本还是音频、图片等等,这些文件最终都会储存为由1和0组成的序列。首先我会介绍这些是如何用二进制表示的。然后,我会介绍计算机

从二进制到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 ,就是先把 ab 的值放在寄存器中,然后把这两个值放在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 指令。例如这一段程序

代码语言:javascript代码运行次数:0运行复制
 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 指令。把上面的例子拓展一下

代码语言:javascript代码运行次数:0运行复制
 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行的代码先对 x9 进行了比较,把比较结果存起来,然后第八行上面的结果进行判断,大于 greater 的化就跳转到 1e ,就是 return 0,小于的化就加 1

循环结构和也是一样的,只不过它会跳转到判断的上方。当 x < 10 时,程序会一遍又一遍循环,到机器码,他会一边又一遍从第7行执行到第10行。

代码语言:javascript代码运行次数:0运行复制
 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

有了选择、判断和循环,程序就可以写出来了。

现在,编译器帮助我们生成机器码,我们使用 CC++Python 这样的高级语言,虽然不用直接接触到这些机器码,不过这些内容对代码调试、理解栈溢出、代码执行的效率等等都比较关键。

2020@Fu_Qingchen

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2021-01-15 ,如有侵权请联系 cloudcommunity@tencent 删除计算机内存数据程序二进制

发布者:admin,转转请注明出处:http://www.yc00.com/web/1748001172a4717441.html

相关推荐

  • 从二进制到HelloWorld

    引言这个内容是从二进制到HelloWorld,介绍二进制代码是如何变成我们的程序的。我们都知道任何文件,无论是代码、文本还是音频、图片等等,这些文件最终都会储存为由1和0组成的序列。首先我会介绍这些是如何用二进制表示的。然后,我会介绍计算机

    5小时前
    10

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信