剑灵服务端源码分享网站,剑灵服务端架设教程

很多朋友对于剑灵服务端源码分享网站和剑灵服务端架设教程不太懂,今天就由小编来为大家分享,希望可以帮助到大家,下面一起来看看吧!

我们每个程序员或许都有一个梦,那就是成为大牛,我们或许都沉浸在各种框架中,以为框架就是一切,以为应用层才是最重要的,你错了。在当今计算机行业中,会应用是基本素质,如果你懂其原理才能让你在行业中走的更远,而计算机基础知识又是重中之重。下面,跟随我的脚步,为你介绍一下计算机底层知识。

CPU

还不了解CPU吗?现在就带你了解一下CPU是什么

CPU的全称是CentralProcessingUnit,它是你的电脑中最硬核的组件,这种说法一点不为过。CPU是能够让你的计算机叫计算机的核心组件,但是它却不能代表你的电脑,CPU与计算机的关系就相当于大脑和人的关系。CPU的核心是从程序或应用程序获取指令并执行计算。此过程可以分为三个关键阶段:提取,解码和执行。CPU从系统的主存中提取指令,然后解码该指令的实际内容,然后再由CPU的相关部分执行该指令。

CPU内部处理过程

下图展示了一般程序的运行流程(以C语言为例),可以说了解程序的运行流程是掌握程序运行机制的基础和前提。

在这个流程中,CPU负责的就是解释和运行最终转换成机器语言的内容。

CPU主要由两部分构成:控制单元和算术逻辑单元(ALU)

控制单元:从内存中提取指令并解码执行算数逻辑单元(ALU):处理算数和逻辑运算

CPU是计算机的心脏和大脑,它和内存都是由许多晶体管组成的电子部件。它接收数据输入,执行指令并处理信息。它与输入/输出(I/O)设备进行通信,这些设备向CPU发送数据和从CPU接收数据。

从功能来看,CPU的内部由寄存器、控制器、运算器和时钟四部分组成,各部分之间通过电信号连通。

寄存器是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个CPU内部会有20-100个寄存器。控制器负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机运算器负责运算从内存中读入寄存器的数据时钟负责发出CPU开始计时的时钟信号

CPU是一系列寄存器的集合体

在CPU的四个结构中,我们程序员只需要了解寄存器就可以了,其余三个不用过多关注,为什么这么说?因为程序是把寄存器作为对象来描述的。

不同类型的CPU,其内部寄存器的种类,数量以及寄存器存储的数值范围都是不同的。不过,根据功能的不同,可以将寄存器划分为下面这几类

种类功能累加寄存器存储运行的数据和运算后的数据。标志寄存器用于反应处理器的状态和运算结果的某些特征以及控制指令的执行。程序计数器程序计数器是用于存放下一条指令所在单元的地址的地方。基址寄存器存储数据内存的起始位置变址寄存器存储基址寄存器的相对地址通用寄存器存储任意数据指令寄存器储存正在被运行的指令,CPU内部使用,程序员无法对该寄存器进行读写栈寄存器存储栈区域的起始位置

其中程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他寄存器一般有多个。

下面就对各个寄存器进行说明

程序计数器

程序计数器(ProgramCounter)是用来存储下一条指令所在单元的地址。

程序执行时,PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。

我们还是以一个事例为准来详细的看一下程序计数器的执行过程

这是一段进行相加的操作,程序启动,在经过编译解析后会由操作系统把硬盘中的程序复制到内存中,示例中的程序是将123和456执行相加操作,并将结果输出到显示器上。

地址0100是程序运行的起始位置。Windows等操作系统把程序从硬盘复制到内存后,会将程序计数器作为设定为起始位置0100,然后执行程序,每执行一条指令后,程序计数器的数值会增加1(或者直接指向下一条指令的地址),然后,CPU就会根据程序计数器的数值,从内存中读取命令并执行,也就是说,程序计数器控制着程序的流程

条件分支和循环机制

高级语言中的条件控制流程主要分为三种:顺序执行、条件分支、循环判断三种,顺序执行是按照地址的内容顺序的执行指令。条件分支是根据条件执行任意地址的指令。循环是重复执行同一地址的指令。

顺序执行的情况比较简单,每执行一条指令程序计数器的值就是+1。条件和循环分支会使程序计数器的值指向任意的地址,这样一来,程序便可以返回到上一个地址来重复执行同一个指令,或者跳转到任意指令。

下面以条件分支为例来说明程序的执行过程(循环也很相似)

程序的开始过程和顺序流程是一样的,CPU从0100处开始执行命令,在0100和0101都是顺序执行,PC的值顺序+1,执行到0102地址的指令时,判断0106寄存器的数值大于0,跳转(jump)到0104地址的指令,将数值输出到显示器中,然后结束程序,0103的指令被跳过了,这就和我们程序中的if()判断是一样的,在不满足条件的情况下,指令会直接跳过。所以PC的执行过程也就没有直接+1,而是下一条指令的地址。

标志寄存器

条件和循环分支会使用到jump(跳转指令),会根据当前的指令来判断是否跳转,上面我们提到了标志寄存器,无论当前累加寄存器的运算结果是正数、负数还是零,标志寄存器都会将其保存

CPU在进行运算时,标志寄存器的数值会根据当前运算的结果自动设定,运算结果的正、负和零三种状态由标志寄存器的三个位表示。标志寄存器的第一个字节位、第二个字节位、第三个字节位各自的结果都为1时,分别代表着正数、零和负数。

CPU的执行机制比较有意思,假设累加寄存器中存储的XXX和通用寄存器中存储的YYY做比较,执行比较的背后,CPU的运算机制就会做减法运算。而无论减法运算的结果是正数、零还是负数,都会保存到标志寄存器中。结果为正表示XXX比YYY大,结果为零表示XXX和YYY相等,结果为负表示XXX比YYY小。程序比较的指令,实际上是在CPU内部做减法运算。

函数调用机制

接下来,我们继续介绍函数调用机制,哪怕是高级语言编写的程序,函数调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。函数执行跳转指令后,必须进行返回处理,单纯的指令跳转没有意义,下面是一个实现函数跳转的例子

图中将变量a和b分别赋值为123和456,调用MyFun(a,b)方法,进行指令跳转。图中的地址是将C语言编译成机器语言后运行时的地址,由于1行C程序在编译后通常会变为多行机器语言,所以图中的地址是分散的。在执行完MyFun(a,b)指令后,程序会返回到MyFun(a,b)的下一条指令,CPU继续执行下面的指令。

函数的调用和返回很重要的两个指令是call和return指令,再将函数的入口地址设定到程序计数器之前,call指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行return指令。return指令的功能是把保存在栈中的地址设定到程序计数器。MyFun函数在被调用之前,0154地址保存在栈中,MyFun函数处理完成后,会把0154的地址保存在程序计数器中。这个调用过程如下

在一些高级语言的条件或者循环语句中,函数调用的处理会转换成call指令,函数结束后的处理则会转换成return指令。

通过地址和索引实现数组

接下来我们看一下基址寄存器和变址寄存器,通过这两个寄存器,我们可以对主存上的特定区域进行划分,来实现类似数组的操作,首先,我们用十六进制数将计算机内存上的00000000-FFFFFFFF的地址划分出来。那么,凡是该范围的内存地址,只要有一个32位的寄存器,便可查看全部地址。但如果想要想数组那样分割特定的内存区域以达到连续查看的目的的话,使用两个寄存器会更加方便。

例如,我们用两个寄存器(基址寄存器和变址寄存器)来表示内存的值

这种表示方式很类似数组的构造,数组是指同样长度的数据在内存中进行连续排列的数据构造。用数组名表示数组全部的值,通过索引来区分数组的各个数据元素,例如:a[0]-a[4],[]内的0-4就是数组的下标。

CPU指令执行过程

几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回

取指令阶段是将内存中的指令读取到CPU中寄存器的过程,程序寄存器用于存储下一条指令所在的地址指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。结果写回阶段,作为最后一个阶段,结果写回(WriteBack,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取;

内存

CPU和内存就像是一堆不可分割的恋人一样,是无法拆散的一对儿,没有内存,CPU无法执行程序指令,那么计算机也就失去了意义;只有内存,无法执行指令,那么计算机照样无法运行。

那么什么是内存呢?内存和CPU如何进行交互?下面就来介绍一下

什么是内存

内存(Memory)是计算机中最重要的部件之一,它是程序与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存对计算机的影响非常大,内存又被称为主存,其作用是存放CPU中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。

内存的物理结构

内存的内部是由各种IC电路组成的,它的种类很庞大,但是其主要分为三种存储器

随机存储器(RAM):内存中最重要的一种,表示既可以从中读取数据,也可以写入数据。当机器关闭时,内存中的信息会丢失。只读存储器(ROM):ROM一般只能用于数据的读取,不能写入数据,但是当机器停电时,这些数据不会丢失。高速缓存(Cache):Cache也是我们经常见到的,它分为一级缓存(L1Cache)、二级缓存(L2Cache)、三级缓存(L3Cache)这些数据,它位于内存和CPU之间,是一个读写速度比内存更快的存储器。当CPU向内存写入数据时,这些数据也会被写入高速缓存中。当CPU需要读取数据时,会直接从高速缓存中直接读取,当然,如需要的数据在Cache中没有,CPU会再去读取内存中的数据。

内存IC是一个完整的结构,它内部也有电源、地址信号、数据信号、控制信号和用于寻址的IC引脚来进行数据的读写。下面是一个虚拟的IC引脚示意图

图中VCC和GND表示电源,A0-A9是地址信号的引脚,D0-D7表示的是控制信号、RD和WR都是好控制信号,我用不同的颜色进行了区分,将电源连接到VCC和GND后,就可以对其他引脚传递0和1的信号,大多数情况下,+5V表示1,0V表示0

我们都知道内存是用来存储数据,那么这个内存IC中能存储多少数据呢?D0-D7表示的是数据信号,也就是说,一次可以输入输出8bit=1byte的数据。A0-A9是地址信号共十个,表示可以指定0000000000-1111111111共2的10次方=1024个地址。每个地址都会存放1byte的数据,因此我们可以得出内存IC的容量就是1KB。

内存的读写过程

让我们把关注点放在内存IC对数据的读写过程上来吧!我们来看一个对内存IC进行数据写入和读取的模型

来详细描述一下这个过程,假设我们要向内存IC中写入1byte的数据的话,它的过程是这样的:

首先给VCC接通+5V的电源,给GND接通0V的电源,使用A0-A9来指定数据的存储场所,然后再把数据的值输入给D0-D7的数据信号,并把WR(write)的值置为1,执行完这些操作后,即可以向内存IC写入数据读出数据时,只需要通过A0-A9的地址信号指定数据的存储场所,然后再将RD的值置为1即可。图中的RD和WR又被称为控制信号。其中当WR和RD都为0时,无法进行写入和读取操作。

内存的现实模型

为了便于记忆,我们把内存模型映射成为我们现实世界的模型,在现实世界中,内存的模型很想我们生活的楼房。在这个楼房中,1层可以存储一个字节的数据,楼层号就是地址,下面是内存和楼层整合的模型图

我们知道,程序中的数据不仅只有数值,还有数据类型的概念,从内存上来看,就是占用内存大小(占用楼层数)的意思。即使物理上强制以1个字节为单位来逐一读写数据的内存,在程序中,通过指定其数据类型,也能实现以特定字节数为单位来进行读写。

二进制

我们都知道,计算机的底层都是使用二进制数据进行数据流传输的,那么为什么会使用二进制表示计算机呢?或者说,什么是二进制数呢?在拓展一步,如何使用二进制进行加减乘除?下面就来看一下

什么是二进制数

那么什么是二进制数呢?为了说明这个问题,我们先把00100111这个数转换为十进制数看一下,二进制数转换为十进制数,直接将各位置上的值*位权即可,那么我们将上面的数值进行转换

也就是说,二进制数代表的00100111转换成十进制就是39,这个39并不是3和9两个数字连着写,而是3*10+9*1,这里面的10,1就是位权,以此类推,上述例子中的位权从高位到低位依次就是76543210。这个位权也叫做次幂,那么最高位就是2的7次幂,2的6次幂等等。二进制数的运算每次都会以2为底,这个2指得就是基数,那么十进制数的基数也就是10。在任何情况下位权的值都是数的位数-1,那么第一位的位权就是1-1=0,第二位的位权就睡2-1=1,以此类推。

那么我们所说的二进制数其实就是用0和1两个数字来表示的数,它的基数为2,它的数值就是每个数的位数*位权再求和得到的结果,我们一般来说数值指的就是十进制数,那么它的数值就是3*10+9*1=39。

移位运算和乘除的关系

在了解过二进制之后,下面我们来看一下二进制的运算,和十进制数一样,加减乘除也适用于二进制数,只要注意逢2进位即可。二进制数的运算,也是计算机程序所特有的运算,因此了解二进制的运算是必须要掌握的。

首先我们来介绍移位运算,移位运算是指将二进制的数值的各个位置上的元素坐左移和右移操作,见下图

补数

刚才我们没有介绍右移的情况,是因为右移之后空出来的高位数值,有0和1两种形式。要想区分什么时候补0什么时候补1,首先就需要掌握二进制数表示负数的方法。

二进制数中表示负数值时,一般会把最高位作为符号来使用,因此我们把这个最高位当作符号位。符号位是0时表示正数,是1时表示负数。那么-1用二进制数该如何表示呢?可能很多人会这么认为:因为1的二进制数是00000001,最高位是符号位,所以正确的表示-1应该是10000001,但是这个答案真的对吗?

计算机世界中是没有减法的,计算机在做减法的时候其实就是在做加法,也就是用加法来实现的减法运算。比如100-50,其实计算机来看的时候应该是100+(-50),为此,在表示负数的时候就要用到二进制补数,补数就是用正数来表示的负数。

为了获得补数,我们需要将二进制的各数位的数值全部取反,然后再将结果+1即可,先记住这个结论,下面我们来演示一下。

具体来说,就是需要先获取某个数值的二进制数,然后对二进制数的每一位做取反操作(0—>1,1—>0),最后再对取反后的数+1,这样就完成了补数的获取。

补数的获取,虽然直观上不易理解,但是逻辑上却非常严谨,比如我们来看一下1-1的这个过程,我们先用上面的这个10000001(它是1的补数,不知道的请看上文,正确性先不管,只是用来做一下计算)来表示一下

奇怪,1-1会变成130,而不是0,所以可以得出结论10000001表示-1是完全错误的。

那么正确的该如何表示呢?其实我们上面已经给出结果了,那就是11111111,来论证一下它的正确性

我们可以看到1-1其实实际上就是1+(-1),对-1进行上面的取反+1后变为11111111,然后与1进行加法运算,得到的结果是九位的100000000,结果发生了溢出,计算机会直接忽略掉溢出位,也就是直接抛掉最高位1,变为00000000。也就是0,结果正确,所以11111111表示的就是-1。

所以负数的二进制表示就是先求其补数,补数的求解过程就是对原始数值的二进制数各位取反,然后将结果+1

算数右移和逻辑右移的区别

在了解完补数后,我们重新考虑一下右移这个议题,右移在移位后空出来的最高位有两种情况0和1。

将二进制数作为带符号的数值进行右移运算时,移位后需要在最高位填充移位前符号位的值(0或1)。这就被称为算数右移。如果数值使用补数表示的负数值,那么右移后在空出来的最高位补1,就可以正确的表示1/2,1/4,1/8等的数值运算。如果是正数,那么直接在空出来的位置补0即可。

下面来看一个右移的例子。将-4右移两位,来各自看一下移位示意图

如上图所示,在逻辑右移的情况下,-4右移两位会变成63,显然不是它的1/4,所以不能使用逻辑右移,那么算数右移的情况下,右移两位会变为-1,显然是它的1/4,故而采用算数右移。

那么我们可以得出来一个结论:左移时,无论是图形还是数值,移位后,只需要将低位补0即可;右移时,需要根据情况判断是逻辑右移还是算数右移。

下面介绍一下符号扩展:将数据进行符号扩展是为了产生一个位数加倍、但数值大小不变的结果,以满足有些指令对操作数位数的要求,例如倍长于除数的被除数,再如将数据位数加长以减少计算过程中的误差。

以8位二进制为例,符号扩展就是指在保持值不变的前提下将其转换成为16位和32位的二进制数。将01111111这个正的8位二进制数转换成为16位二进制数时,很容易就能够得出0000000001111111这个正确的结果,但是像11111111这样的补数来表示的数值,该如何处理?直接将其表示成为1111111111111111就可以了。也就是说,不管正数还是补数表示的负数,只需要将0和1填充高位即可。

内存和磁盘的关系

我们大家知道,计算机的五大基础部件是存储器、控制器、运算器、输入和输出设备,其中从存储功能的角度来看,可以把存储器分为内存和磁盘,我们上面介绍过内存,下面就来介绍一下磁盘以及磁盘和内存的关系

程序不读入内存就无法运行

计算机最主要的存储部件是内存和磁盘。磁盘中存储的程序必须加载到内存中才能运行,在磁盘中保存的程序是无法直接运行的,这是因为负责解析和运行程序内容的CPU是需要通过程序计数器来指定内存地址从而读出程序指令的。

磁盘构造

磁盘缓存

我们上面提到,磁盘往往和内存是互利共生的关系,相互协作,彼此持有良好的合作关系。每次内存都需要从磁盘中读取数据,必然会读到相同的内容,所以一定会有一个角色负责存储我们经常需要读到的内容。我们大家做软件的时候经常会用到缓存技术,那么硬件层面也不例外,磁盘也有缓存,磁盘的缓存叫做磁盘缓存。

磁盘缓存指的是把从磁盘中读出的数据存储到内存的方式,这样一来,当接下来需要读取相同的内容时,就不会再通过实际的磁盘,而是通过磁盘缓存来读取。某一种技术或者框架的出现势必要解决某种问题的,那么磁盘缓存就大大改善了磁盘访问的速度

虚拟内存

虚拟内存是内存和磁盘交互的第二个媒介。虚拟内存是指把磁盘的一部分作为假想内存来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个完整的地址空间),但是实际上,它通常被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。

通过借助虚拟内存,在内存不足时仍然可以运行程序。例如,在只剩5MB内存空间的情况下仍然可以运行10MB的程序。由于CPU只能执行加载到内存中的程序,因此,虚拟内存的空间就需要和内存中的空间进行置换(swap),然后运行程序。

虚拟内存与内存的交换方式

虚拟内存的方法有分页式和分段式两种。Windows采用的是分页式。该方式是指在不考虑程序构造的情况下,把运行的程序按照一定大小的页进行分割,并以页为单位进行置换。在分页式中,我们把磁盘的内容读到内存中称为PageIn,把内存的内容写入磁盘称为PageOut。Windows计算机的页大小为4KB,也就是说,需要把应用程序按照4KB的页来进行切分,以页(page)为单位放到磁盘中,然后进行置换。

为了实现内存功能,Windows在磁盘上提供了虚拟内存使用的文件(pagefile,页文件)。该文件由Windows生成和管理,文件的大小和虚拟内存大小相同,通常大小是内存的1-2倍。

磁盘的物理结构

之前我们介绍了CPU、内存的物理结构,现在我们来介绍一下磁盘的物理结构。磁盘的物理结构指的是磁盘存储数据的形式

磁盘是通过其物理表面划分成多个空间来使用的。划分的方式有两种:可变长方式和扇区方式。前者是将物理结构划分成长度可变的空间,后者是将磁盘结构划分为固定长度的空间。一般Windows所使用的硬盘和软盘都是使用扇区这种方式。扇区中,把磁盘表面分成若干个同心圆的空间就是磁道,把磁道按照固定大小的存储空间划分而成的就是扇区

扇区是对磁盘进行物理读写的最小单位。Windows中使用的磁盘,一般是一个扇区512个字节。不过,Windows在逻辑方面对磁盘进行读写的单位是扇区整数倍簇。根据磁盘容量不同功能,1簇可以是512字节(1簇=1扇区)、1KB(1簇=2扇区)、2KB、4KB、8KB、16KB、32KB(1簇=64扇区)。簇和扇区的大小是相等的。

压缩算法

我们想必都有过压缩和解压缩文件的经历,当文件太大时,我们会使用文件压缩来降低文件的占用空间。比如微信上传文件的限制是100MB,我这里有个文件夹无法上传,但是我解压完成后的文件一定会小于100MB,那么我的文件就可以上传了。

此外,我们把相机拍完的照片保存到计算机上的时候,也会使用压缩算法进行文件压缩,文件压缩的格式一般是JPEG。

那么什么是压缩算法呢?压缩算法又是怎么定义的呢?在认识算法之前我们需要先了解一下文件是如何存储的

文件存储

文件是将数据存储在磁盘等存储媒介的一种形式。程序文件中最基本的存储数据单位是字节。文件的大小不管是xxxKB、xxxMB等来表示,就是因为文件是以字节B=Byte为单位来存储的。

文件就是字节数据的集合。用1字节(8位)表示的字节数据有256种,用二进制表示的话就是00000000-11111111。如果文件中存储的数据是文字,那么该文件就是文本文件。如果是图形,那么该文件就是图像文件。在任何情况下,文件中的字节数都是连续存储的。

压缩算法的定义

上面介绍了文件的集合体其实就是一堆字节数据的集合,那么我们就可以来给压缩算法下一个定义。

压缩算法(compactionalgorithm)指的就是数据压缩的算法,主要包括压缩和还原(解压缩)的两个步骤。

其实就是在不改变原有文件属性的前提下,降低文件字节空间和占用空间的一种算法。

根据压缩算法的定义,我们可将其分成不同的类型:

有损和无损

无损压缩:能够无失真地从压缩后的数据重构,准确地还原原始数据。可用于对数据的准确性要求严格的场合,如可执行文件和普通文件的压缩、磁盘的压缩,也可用于多媒体数据的压缩。该方法的压缩比较小。如差分编码、RLE、Huffman编码、LZW编码、算术编码。

有损压缩:有失真,不能完全准确地恢复原始数据,重构的数据只是原始数据的一个近似。可用于对数据的准确性要求不高的场合,如多媒体数据的压缩。该方法的压缩比较大。例如预测编码、音感编码、分形压缩、小波压缩、JPEG/MPEG。

对称性

如果编解码算法的复杂性和所需时间差不多,则为对称的编码方法,多数压缩算法都是对称的。但也有不对称的,一般是编码难而解码容易,如Huffman编码和分形编码。但用于密码学的编码方法则相反,是编码容易,而解码则非常难。

帧间与帧内

在视频编码中会同时用到帧内与帧间的编码方法,帧内编码是指在一帧图像内独立完成的编码方法,同静态图像的编码,如JPEG;而帧间编码则需要参照前后帧才能进行编解码,并在编码过程中考虑对帧之间的时间冗余的压缩,如MPEG。

实时性

在有些多媒体的应用场合,需要实时处理或传输数据(如现场的数字录音和录影、播放MP3/RM/VCD/DVD、视频/音频点播、网络现场直播、可视电话、视频会议),编解码一般要求延时≤50ms。这就需要简单/快速/高效的算法和高速/复杂的CPU/DSP芯片。

分级处理

有些压缩算法可以同时处理不同分辨率、不同传输速率、不同质量水平的多媒体数据,如JPEG2000、MPEG-2/4。

这些概念有些抽象,主要是为了让大家了解一下压缩算法的分类,下面我们就对具体的几种常用的压缩算法来分析一下它的特点和优劣

几种常用压缩算法的理解

RLE算法的机制

接下来就让我们正式看一下文件的压缩机制。首先让我们来尝试对AAAAAABBCDDEEEEEF这17个半角字符的文件(文本文件)进行压缩。虽然这些文字没有什么实际意义,但是很适合用来描述RLE的压缩机制。

由于半角字符(其实就是英文字符)是作为1个字节保存在文件中的,所以上述的文件的大小就是17字节。如图

那么,如何才能压缩该文件呢?大家不妨也考虑一下,只要是能够使文件小于17字节,我们可以使用任何压缩算法。

最显而易见的一种压缩方式我觉得你已经想到了,就是把相同的字符去重化,也就是字符*重复次数的方式进行压缩。所以上面文件压缩后就会变成下面这样

从图中我们可以看出,AAAAAABBCDDEEEEEF的17个字符成功被压缩成了A6B2C1D2E5F1的12个字符,也就是12/17=70%,压缩比为70%,压缩成功了。

像这样,把文件内容用数据*重复次数的形式来表示的压缩方法成为RLE(RunLengthEncoding,行程长度编码)算法。RLE算法是一种很好的压缩方法,经常用于压缩传真的图像等。因为图像文件的本质也是字节数据的集合体,所以可以用RLE算法进行压缩

哈夫曼算法和莫尔斯编码

下面我们来介绍另外一种压缩算法,即哈夫曼算法。在了解哈夫曼算法之前,你必须舍弃半角英文数字的1个字符是1个字节(8位)的数据。下面我们就来认识一下哈夫曼算法的基本思想。

文本文件是由不同类型的字符组合而成的,而且不同字符出现的次数也是不一样的。例如,在某个文本文件中,A出现了100次左右,Q仅仅用到了3次,类似这样的情况很常见。哈夫曼算法的关键就在于多次出现的数据用小于8位的字节数表示,不常用的数据则可以使用超过8位的字节数表示。A和Q都用8位来表示时,原文件的大小就是100次*8位+3次*8位=824位,假设A用2位,Q用10位来表示就是2*100+3*10=230位。

不过要注意一点,最终磁盘的存储都是以8位为一个字节来保存文件的。

哈夫曼算法比较复杂,在深入了解之前我们先吃点甜品,了解一下莫尔斯编码,你一定看过美剧或者战争片的电影,在战争中的通信经常采用莫尔斯编码来传递信息,例如下面

接下来我们来讲解一下莫尔斯编码,下面是莫尔斯编码的示例,大家把1看作是短点(嘀),把11看作是长点(嗒)即可。

莫尔斯编码一般把文本中出现最高频率的字符用短编码来表示。如表所示,假如表示短点的位是1,表示长点的位是11的话,那么E(嘀)这一数据的字符就可以用1来表示,C(滴答滴答)就可以用9位的110101101来表示。在实际的莫尔斯编码中,如果短点的长度是1,长点的长度就是3,短点和长点的间隔就是1。这里的长度指的就是声音的长度。比如我们想用上面的AAAAAABBCDDEEEEEF例子来用莫尔斯编码重写,在莫尔斯曼编码中,各个字符之间需要加入表示时间间隔的符号。这里我们用00加以区分。

所以,AAAAAABBCDDEEEEEF这个文本就变为了A*6次+B*2次+C*1次+D*2次+E*5次+F*1次+字符间隔*16=4位*6次+8位*2次+9位*1次+6位*2次+1位*5次+8位*1次+2位*16次=106位=14字节。

所以使用莫尔斯电码的压缩比为14/17=82%。效率并不太突出。

用二叉树实现哈夫曼算法

刚才已经提到,莫尔斯编码是根据日常文本中各字符的出现频率来决定表示各字符的编码数据长度的。不过,在该编码体系中,对AAAAAABBCDDEEEEEF这种文本来说并不是效率最高的。

下面我们来看一下哈夫曼算法。哈夫曼算法是指,为各压缩对象文件分别构造最佳的编码体系,并以该编码体系为基础来进行压缩。因此,用什么样的编码(哈夫曼编码)对数据进行分割,就要由各个文件而定。用哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据。

接下来,我们在对AAAAAABBCDDEEEEEF中的A-F这些字符,按照出现频率高的字符用尽量少的位数编码来表示这一原则进行整理。按照出现频率从高到低的顺序整理后,结果如下,同时也列出了编码方案。

字符出现频率编码(方案)位数A601E511B2102D2112C11003F11013

在上表的编码方案中,随着出现频率的降低,字符编码信息的数据位数也在逐渐增加,从最开始的1位、2位依次增加到3位。不过这个编码体系是存在问题的,你不知道100这个3位的编码,它的意思是用1、0、0这三个编码来表示E、A、A呢?还是用10、0来表示B、A呢?还是用100来表示C呢。

而在哈夫曼算法中,通过借助哈夫曼树的构造编码体系,即使在不使用字符区分符号的情况下,也可以构建能够明确进行区分的编码体系。不过哈夫曼树的算法要比较复杂,下面是一个哈夫曼树的构造过程。

自然界树的从根开始生叶的,而哈夫曼树则是叶生枝

哈夫曼树能够提升压缩比率

使用哈夫曼树之后,出现频率越高的数据所占用的位数越少,这也是哈夫曼树的核心思想。通过上图的步骤二可以看出,枝条连接数据时,我们是从出现频率较低的数据开始的。这就意味着出现频率低的数据到达根部的枝条也越多。而枝条越多则意味着编码的位数随之增加。

接下来我们来看一下哈夫曼树的压缩比率,用上图得到的数据表示AAAAAABBCDDEEEEEF为0000000000001001001101011010101010101111,40位=5字节。压缩前的数据是17字节,压缩后的数据竟然达到了惊人的5字节,也就是压缩比率=5/17=29%如此高的压缩率,简直是太惊艳了。

大家可以参考一下,无论哪种类型的数据,都可以用哈夫曼树作为压缩算法

文件类型压缩前压缩后压缩比率文本文件14862字节4119字节28%图像文件96062字节9456字节10%EXE文件24576字节4652字节19%

可逆压缩和非可逆压缩

最后,我们来看一下图像文件的数据形式。图像文件的使用目的通常是把图像数据输出到显示器、打印机等设备上。常用的图像格式有:BMP、JPEG、TIFF、GIF格式等。

BMP:是使用Windows自带的画笔来做成的一种图像形式JPEG:是数码相机等常用的一种图像数据形式TIFF:是一种通过在文件中包含&34;就能够快速显示出数据性质的图像形式GIF:是由美国开发的一种数据形式,要求色数不超过256个

图像文件可以使用前面介绍的RLE算法和哈夫曼算法,因为图像文件在多数情况下并不要求数据需要还原到和压缩之前一摸一样的状态,允许丢失一部分数据。我们把能还原到压缩前状态的压缩称为可逆压缩,无法还原到压缩前状态的压缩称为非可逆压缩。

一般来说,JPEG格式的文件是非可逆压缩,因此还原后有部分图像信息比较模糊。GIF是可逆压缩

操作系统

操作系统环境

程序中包含着运行环境这一内容,可以说运行环境=操作系统+硬件,操作系统又可以被称为软件,它是由一系列的指令组成的。我们不介绍操作系统,我们主要来介绍一下硬件的识别。

我们肯定都玩儿过游戏,你玩儿游戏前需要干什么?是不是需要先看一下自己的笔记本或者电脑是不是能肝的起游戏?下面是一个游戏的配置(怀念一下wow)

图中的主要配置如下

操作系统版本:说的就是应用程序运行在何种系统环境,现在市面上主要有三种操作系统环境,Windows、Linux和Unix,一般我们玩儿的大型游戏几乎都是在Windows上运行,可以说Windows是游戏的天堂。Windows操作系统也会有区分,分为32位操作系统和64位操作系统,互不兼容。处理器:处理器指的就是CPU,你的电脑的计算能力,通俗来讲就是每秒钟能处理的指令数,如果你的电脑觉得卡带不起来的话,很可能就是CPU的计算能力不足导致的。想要加深理解,请阅读博主的另一篇文章:程序员需要了解的硬核知识之CPU显卡:显卡承担图形的输出任务,因此又被称为图形处理器(GraphicProcessingUnit,GPU),显卡也非常重要,比如我之前玩儿的剑灵开五档(其实就是图像变得更清晰)会卡,其实就是显卡显示不出来的原因。内存:内存即主存,就是你的应用程序在运行时能够动态分析指令的这部分存储空间,它的大小也能决定你电脑的运行速度,想要加深理解,请阅读博主的另一篇文章程序员需要了解的硬核知识之内存存储空间:存储空间指的就是应用程序安装所占用的磁盘空间,由图中可知,此游戏的最低存储空间必须要大于5GB,其实我们都会遗留很大一部分用来安装游戏。

从程序的运行环境这一角度来考量的话,CPU的种类是特别重要的参数,为了使程序能够正常运行,必须满足CPU所需的最低配置。

CPU只能解释其自身固有的语言。不同的CPU能解释的机器语言的种类也是不同的。机器语言的程序称为本地代码(nativecode),程序员用C等高级语言编写的程序,仅仅是文本文件。文本文件(排除文字编码的问题)在任何环境下都能显示和编辑。我们称之为源代码。通过对源代码进行编译,就可以得到本地代码。下图反映了这个过程。

uploading-image-703074.png

Windows操作系统克服了CPU以外的硬件差异

计算机的硬件并不仅仅是由CPU组成的,还包括用于存储程序指令的数据和内存,以及通过I/O连接的键盘、显示器、硬盘、打印机等外围设备。

在WIndows软件中,键盘输入、显示器输出等并不是直接向硬件发送指令。而是通过向Windows发送指令实现的。因此,程序员就不用注意内存和I/O地址的不同构成了。Windows操作的是硬件而不是软件,软件通过操作Windows系统可以达到控制硬件的目的。

不同操作系统的API差异性

接下来我们看一下操作系统的种类。同样机型的计算机,可安装的操作系统类型也会有多种选择。例如:AT兼容机除了可以安装Windows之外,还可以采用Unix系列的Linux以及FreeBSD(也是一种Unix操作系统)等多个操作系统。当然,应用软件则必须根据不同的操作系统类型来专门开发。CPU的类型不同,所对应机器的语言也不同,同样的道理,操作系统的类型不同,应用程序向操作系统传递指令的途径也不同

应用程序向系统传递指令的途径称为API(ApplicationProgrammingInterface)。Windows以及Linux操作系统的API,提供了任何应用程序都可以利用的函数组合。因为不同操作系统的API是有差异的。所以,如何要将同样的应用程序移植到另外的操作系统,就必须要覆盖应用所用到的API部分。

键盘输入、鼠标输入、显示器输出、文件输入和输出等同外围设备进行交互的功能,都是通过API提供的。

这也就是为什么Windows应用程序不能直接移植到Linux操作系统上的原因,API差异太大了。

在同类型的操作系统下,不论硬件如何,API几乎相同。但是,由于不同种类CPU的机器语言不同,因此本地代码也不尽相同。

操作系统功能的历史

操作系统其实也是一种软件,任何新事物的出现肯定都有它的历史背景,那么操作系统也不是凭空出现的,肯定有它的历史背景。

在计算机尚不存在操作系统的年代,完全没有任何程序,人们通过各种按钮来控制计算机,这一过程非常麻烦。于是,有人开发出了仅具有加载和运行功能的监控程序,这就是操作系统的原型。通过事先启动监控程序,程序员可以根据需要将各种程序加载到内存中运行。虽然仍旧比较麻烦,但比起在没有任何程序的状态下进行开发,工作量得到了很大的缓解。

随着时代的发展,人们在利用监控程序编写程序的过程中发现很多程序都有公共的部分。例如,通过键盘进行文字输入,显示器进行数据展示等,如果每编写一个新的应用程序都需要相同的处理的话,那真是太浪费时间了。因此,基本的输入输出部分的程序就被追加到了监控程序中。初期的操作系统就是这样诞生了。

类似的想法可以共用,人们又发现有更多的应用程序可以追加到监控程序中,比如硬件控制程序,编程语言处理器(汇编、编译、解析)以及各种应用程序等,结果就形成了和现在差异不大的操作系统,也就是说,其实操作系统是多个程序的集合体。

Windows操作系统的特征

Windows操作系统是世界上用户数量最庞大的群体,作为Windows操作系统的资深用户,你都知道Windows操作系统有哪些特征吗?下面列举了一些Windows操作系统的特性

Windows操作系统有两个版本:32位和64位通过API函数集成来提供系统调用提供了采用图形用户界面的用户界面通过WYSIWYG实现打印输出,WYSIWYG其实就是WhatYouSeeIsWhatYouGet,值得是显示器上显示的图形和文本都是可以原样输出到打印机打印的。提供多任务功能,即能够同时开启多个任务提供网络功能和数据库功能通过即插即用实现设备驱动的自设定

这些是对程序员来讲比较有意义的一些特征,下面针对这些特征来进行分别的介绍

32位操作系统

这里表示的32位操作系统表示的是处理效率最高的数据大小。Windows处理数据的基本单位是32位。这与最一开始在MS-DOS等16位操作系统不同,因为在16位操作系统中处理32位数据需要两次,而32位操作系统只需要一次就能够处理32位的数据,所以一般在windows上的应用,它们的最高能够处理的数据都是32位的。

比如,用C语言来处理整数数据时,有8位的char类型,16位的short类型,以及32位的long类型三个选项,使用位数较大的long类型进行处理的话,增加的只是内存以及磁盘的开销,对性能影响不大。

现在市面上大部分都是64位操作系统了,64位操作系统也是如此。

通过API函数集来提供系统调用

Windows是通过名为API的函数集来提供系统调用的。API是联系应用程序和操作系统之间的接口,全称叫做ApplicationProgrammingInterface,应用程序接口。

当前主流的32位版WindowsAPI也称为Win32API,之所以这样命名,是需要和不同的操作系统进行区分,比如最一开始的16位版的Win16API,和后来流行的Win64API。

API通过多个DLL文件来提供,各个API的实体都是用C语言编写的函数。所以,在C语言环境下,使用API更加容易,比如API所用到的MessageBox()函数,就被保存在了Windows提供的user32.dll这个DLL文件中。

提供采用了GUI的用户界面

GUI(GraphicalUserInterface)指得就是图形用户界面,通过点击显示器中的窗口以及图标等可视化的用户界面,举个例子:Linux操作系统就有两个版本,一种是简洁版,直接通过命令行控制硬件,还有一种是可视化版,通过光标点击图形界面来控制硬件。

通过WYSIWYG实现打印输出

WYSIWYG指的是显示器上输出的内容可以直接通过打印机打印输出。在Windows中,显示器和打印机被认作同等的图形输出设备处理的,该功能也为WYSIWYG提供了条件。

借助WYSIWYG功能,程序员可以轻松不少。最初,为了是现在显示器中显示和在打印机中打印,就必须分别编写各自的程序,而在Windows中,可以借助WYSIWYG基本上在一个程序中就可以做到显示和打印这两个功能了。

提供多任务功能

多任务指的就是同时能够运行多个应用程序的功能,Windows是通过时钟分割技术来实现多任务功能的。时钟分割指的是短时间间隔内,多个程序切换运行的方式。在用户看来,就好像是多个程序在同时运行,其底层是CPU时间切片,这也是多线程多任务的核心。

提供网络功能和数据库功能

Windows中,网络功能是作为标准功能提供的。数据库(数据库服务器)功能有时也会在后面追加。网络功能和数据库功能虽然并不是操作系统不可或缺的,但因为它们和操作系统很接近,所以被统称为中间件而不是应用。意思是处于操作系统和应用的中间层,操作系统和中间件组合在一起,称为系统软件。应用不仅可以利用操作系统,也可以利用中间件的功能。

相对于操作系统一旦安装就不能轻易更换,中间件可以根据需要进行更换,不过,对于大部分应用来说,更换中间件的话,会造成应用也随之更换,从这个角度来说,更?换中间件也不是那么容易。

通过即插即用实现设备驱动的自动设定

即插即用(Plug-and-Play)指的是新的设备连接(plug)后就可以直接使用的机制,新设备连接计算机后,计算机就会自动安装和设定用来控制该设备的驱动程序

设备驱动是操作系统的一部分,提供了同硬件进行基本的输入输出的功能。键盘、鼠标、显示器、磁盘装置等,这些计算机中必备的硬件的设备驱动,一般都是随操作系统一起安装的。

有时DLL文件也会同设备驱动文件一起安装。这些DLL文件中存储着用来利用该新追加的硬件API,通过API,可以制作出运行该硬件的心应用。

汇编语言和本地代码

我们在之前的文章中探讨过,计算机CPU只能运行本地代码(机器语言)程序,用C语言等高级语言编写的代码,需要经过编译器编译后,转换为本地代码才能够被CPU解释执行。

但是本地代码的可读性非常差,所以需要使用一种能够直接读懂的语言来替换本地代码,那就是在各本地代码中,附带上表示其功能的英文缩写,比如在加法运算的本地代码加上add(addition)的缩写、在比较运算符的本地代码中加上cmp(compare)的缩写等,这些通过缩写来表示具体本地代码指令的标志称为助记符,使用助记符的语言称为汇编语言。这样,通过阅读汇编语言,也能够了解本地代码的含义了。

不过,即使是使用汇编语言编写的源代码,最终也必须要转换为本地代码才能够运行,负责做这项工作的程序称为编译器,转换的这个过程称为汇编。在将源代码转换为本地代码这个功能方面,汇编器和编译器是同样的。

用汇编语言编写的源代码和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言编写的代码。把本地代码转换为汇编代码的这一过程称为反汇编,执行反汇编的程序称为反汇编程序。

哪怕是C语言编写的源代码,编译后也会转换成特定CPU用的本地代码。而将其反汇编的话,就可以得到汇编语言的源代码,并对其内容进行调查。不过,本地代码变成C语言源代码的反编译,要比本地代码转换成汇编代码的反汇编要困难,这是因为,C语言代码和本地代码不是一一对应的关系。

通过编译器输出汇编语言的源代码

我们上面提到本地代码可以经过反汇编转换成为汇编代码,但是只有这一种转换方式吗?显然不是,C语言编写的源代码也能够通过编译器编译称为汇编代码,下面就来尝试一下。

首先需要先做一些准备,需要先下载BorlandC++5.5编译器,为了方便,我这边直接下载好了读者直接从我的百度网盘提取即可(链接:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA密码:hz1u)

下载完毕,需要进行配置,下面是配置说明(https://wenku.baidu.com/view/22e2f418650e52ea551898ad.html),教程很完整跟着配置就可以,下面开始我们的编译过程

首先用Windows记事本等文本编辑器编写如下代码

//返回两个参数值之和的函数\nintAddNum(inta,intb){\nreturna+b;\n}\n\n//调用AddNum函数的函数\nvoidMyFunc(){\nintc;\nc=AddNum(123,456);\n}

编写完成后将其文件名保存为Sample4.c,C语言源文件的扩展名,通常用.c来表示,上面程序是提供两个输入参数并返回它们之和。

在Windows操作系统下打开命令提示符,切换到保存Sample4.c的文件夹下,然后在命令提示符中输入

bcc32-c-SSample4.c

bcc32是启动BorlandC++的命令,-c的选项是指仅进行编译而不进行链接,-S选项被用来指定生成汇编语言的源代码

作为编译的结果,当前目录下会生成一个名为Sample4.asm的汇编语言源代码。汇编语言源文件的扩展名,通常用.asm来表示,下面就让我们用编辑器打开看一下Sample4.asm中的内容

.386p\nifdef??version\nif??versionGT500H\n.mmx\nendif\nendif\nmodelflat\nifndef??version\n?debugmacro\nendm\nendif\n?debugS&34;\n?debugT&34;\n_TEXTsegmentdwordpublicuse32&39;\n_TEXTends\n_DATAsegmentdwordpublicuse32&39;\n_DATAends\n_BSSsegmentdwordpublicuse32&39;\n_BSSends\nDGROUPgroup_BSS,_DATA\n_TEXTsegmentdwordpublicuse32&39;\n_AddNumprocnear\n?live1@0:\n;\n;intAddNum(inta,intb){\n;\npushebp\nmovebp,esp\n;\n;\n;returna+b;\n;\n@1:\nmoveax,dwordptr[ebp+8]\naddeax,dwordptr[ebp+12]\n;\n;}\n;\n@3:\n@2:\npopebp\nret\n_AddNumendp\n_MyFuncprocnear\n?live1@48:\n;\n;voidMyFunc(){\n;\npushebp\nmovebp,esp\n;\n;intc;\n;c=AddNum(123,456);\n;\n@4:\npush456\npush123\ncall_AddNum\naddesp,8\n;\n;}\n;\n@5:\npopebp\nret\n_MyFuncendp\n_TEXTends\npublic_AddNum\npublic_MyFunc\n?debugD&34;2034345835\nend

这样,编译器就成功的把C语言转换成为了汇编代码了。

不会转换成本地代码的伪指令

第一次看到汇编代码的读者可能感觉起来比较难,不过实际上其实比较简单,而且可能比C语言还要简单,为了便于阅读汇编代码的源代码,需要注意几个要点

汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操作码)和针对汇编器的伪指令构成的。伪指令负责把程序的构造以及汇编的方法指示给汇编器(转换程序)。不过伪指令是无法汇编转换成为本地代码的。下面是上面程序截取的伪指令

_TEXTsegmentdwordpublicuse32&39;\n_TEXTends\n_DATAsegmentdwordpublicuse32&39;\n_DATAends\n_BSSsegmentdwordpublicuse32&39;\n_BSSends\nDGROUPgroup_BSS,_DATA\n\n_AddNumprocnear\n_AddNumendp\n\n_MyFuncprocnear\n_MyFuncendp\n\n_TEXTends\nend

由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合体上加一个名字而得到的,称为段定义。段定义的英文表达具有区域的意思,在这个程序中,段定义指的是命令和数据等程序的集合体的意思,一个程序由多个段定义构成。

上面代码的开始位置,定义了3个名称分别为_TEXT、_DATA、_BSS的段定义,_TEXT是指定的段定义,_DATA是被初始化(有初始值)的数据的段定义,_BSS是尚未初始化的数据的段定义。这种定义的名称是由BorlandC++定义的,是由BorlandC++编译器自动分配的,所以程序段定义的顺序就成为了_TEXT、_DATA、_BSS,这样也确保了内存的连续性

_TEXTsegmentdwordpublicuse32&39;\n_TEXTends\n_DATAsegmentdwordpublicuse32&39;\n_DATAends\n_BSSsegmentdwordpublicuse32&39;\n_BSSends

段定义(segment)是用来区分或者划分范围区域的意思。汇编语言的segment伪指令表示段定义的起始,ends伪指令表示段定义的结束。段定义是一段连续的内存空间

而group这个伪指令表示的是将_BSS和_DATA这两个段定义汇总名为DGROUP的组

DGROUPgroup_BSS,_DATA

围起_AddNum和_MyFun的_TEXTsegment和_TEXTends,表示_AddNum和_MyFun是属于_TEXT这一段定义的。

_TEXTsegmentdwordpublicuse32&39;\n_TEXTends

因此,即使在源代码中指令和数据是混杂编写的,经过编译和汇编后,也会转换成为规整的本地代码。

_AddNumproc和_AddNumendp围起来的部分,以及_MyFuncproc和_MyFuncendp围起来的部分,分别表示AddNum函数和MyFunc函数的范围。

_AddNumprocnear\n_AddNumendp\n\n_MyFuncprocnear\n_MyFuncendp

编译后在函数名前附带上下划线_,是BorlandC++的规定。在C语言中编写的AddNum函数,在内部是以_AddNum这个名称处理的。伪指令proc和endp围起来的部分,表示的是过程(procedure)的范围。在汇编语言中,这种相当于C语言的函数的形式称为过程。

末尾的end伪指令,表示的是源代码的结束。

汇编语言的语法是操作码+操作数

在汇编语言中,一行表示一对CPU的一个指令。汇编语言指令的语法结构是操作码+操作数,也存在只有操作码没有操作数的指令。

操作码表示的是指令动作,操作数表示的是指令对象。操作码和操作数一起使用就是一个英文指令。比如从英语语法来分析的话,操作码是动词,操作数是宾语。比如这个句子Givememoney这个英文指令的话,Give就是操作码,me和money就是操作数。汇编语言中存在多个操作数的情况,要用逗号把它们分割,就像是Giveme,money这样。

能够使用何种形式的操作码,是由CPU的种类决定的,下面对操作码的功能进行了整理。

本地代码需要加载到内存后才能运行,内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把数据和指令读出来,然后放在CPU内部的寄存器中进行处理。

如果CPU和内存的关系你还不是很了解的话,请阅读作者的另一篇文章程序员需要了解的硬核知识之CPU详细了解。

寄存器是CPU中的存储区域,寄存器除了具有临时存储和计算的功能之外,还具有运算功能,x86系列的主要种类和角色如下图所示

指令解析

下面就对CPU中的指令进行分析

最常用的mov指令

指令中最常使用的是对寄存器和内存进行数据存储的mov指令,mov指令的两个操作数,分别用来指定数据的存储地和读出源。操作数中可以指定寄存器、常数、标签(附加在地址前),以及用方括号([])围起来的这些内容。如果指定了没有用([])方括号围起来的内容,就表示对该值进行处理;如果指定了用方括号围起来的内容,方括号的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。让我们对上面的代码片段进行说明

movebp,esp\nmoveax,dwordptr[ebp+8]

movebp,esp中,esp寄存器中的值被直接存储在了ebp中,也就是说,如果esp寄存器的值是100的话那么ebp寄存器的值也是100。

而在moveax,dwordptr[ebp+8]这条指令中,ebp寄存器的值+8后会被解析称为内存地址。如果ebp

寄存器的值是100的话,那么eax寄存器的值就是100+8的地址的值。dwordptr也叫做doublewordpointer简单解释一下就是从指定的内存地址中读出4字节的数据

对栈进行push和pop

程序运行时,会在内存上申请分配一个称为栈的数据空间。栈(stack)的特性是后入先出,数据在存储时是从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出时则是按照从上往下进行读取的。

栈是存储临时数据的区域,它的特点是通过push指令和pop指令进行数据的存储和读出。向栈中存储数据称为入栈,从栈中读出数据称为出栈,32位x86系列的CPU中,进行1次push或者pop,即可处理32位(4字节)的数据。

函数的调用机制

下面我们一起来分析一下函数的调用机制,我们以上面的C语言编写的代码为例。首先,让我们从MyFunc函数调用AddNum函数的汇编语言部分开始,来对函数的调用机制进行说明。栈在函数的调用中发挥了巨大的作用,下面是经过处理后的MyFunc函数的汇编处理内容

_MyFuncprocnear\npushebp;将ebp寄存器的值存入栈中(1)\nmovebp,esp;将esp寄存器的值存入ebp寄存器中(2)\npush456;将456入栈(3)\npush123;将123入栈(4)\ncall_AddNum;调用AddNum函数(5)\naddesp,8;esp寄存器的值+8(6)\npopebp;读出栈中的数值存入esp寄存器中(7)\nret;结束MyFunc函数,返回到调用源(8)\n_MyFuncendp

代码解释中的(1)、(2)、(7)、(8)的处理适用于C语言中的所有函数,我们会在后面展示AddNum函数处理内容时进行说明。这里希望大家先关注(3)-(6)这一部分,这对了解函数调用机制至关重要。

(3)和(4)表示的是将传递给AddNum函数的参数通过push入栈。在C语言源代码中,虽然记述为函数AddNum(123,456),但入栈时则会先按照456,123这样的顺序。也就是位于后面的数值先入栈。这是C语言的规定。(5)表示的call指令,会把程序流程跳转到AddNum函数指令的地址处。在汇编语言中,函数名表示的就是函数所在的内存地址。AddNum函数处理完毕后,程序流程必须要返回到编号(6)这一行。call指令运行后,call指令的下一行(也就指的是(6)这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动的push入栈。该值会在AddNum函数处理的最后通过ret指令pop出栈,然后程序会返回到(6)这一行。

(6)部分会把栈中存储的两个参数(456和123)进行销毁处理。虽然通过两次的pop指令也可以实现,不过采用esp寄存器+8的方式会更有效率(处理1次即可)。对栈进行数值的输入和输出时,数值的单位是4字节。因此,通过在负责栈地址管理的esp寄存器中加上4的2倍8,就可以达到和运行两次pop命令同样的效果。虽然内存中的数据实际上还残留着,但只要把esp寄存器的值更新为数据存储地址前面的数据位置,该数据也就相当于销毁了。

我在编译Sample4.c文件时,出现了下图的这条消息

图中的意思是指c的值在MyFunc定义了但是一直未被使用,这其实是一项编译器优化的功能,由于存储着AddNum函数返回值的变量c在后面没有被用到,因此编译器就认为该变量没有意义,进而也就没有生成与之对应的汇编语言代码

下图是调用AddNum这一函数前后栈内存的变化

函数的内部处理

上面我们用汇编代码分析了一下Sample4.c整个过程的代码,现在我们着重分析一下AddNum函数的源代码部分,分析一下参数的接收、返回值和返回等机制

_AddNumprocnear\npushebp———–(1)\nmovebp,esp———–(2)\nmoveax,dwordptr[ebp+8]———–(3)\naddeax,dwordptr[ebp+12]———–(4)\npopebp———–(5)\nret———————————-(6)\n_AddNumendp

ebp寄存器的值在(1)中入栈,在(5)中出栈,这主要是为了把函数中用到的ebp寄存器的内容,恢复到函数调用前的状态。

(2)中把负责管理栈地址的esp寄存器的值赋值到了ebp寄存器中。这是因为,在mov指令中方括号内的参数,是不允许指定esp寄存器的。因此,这里就采用了不直接通过esp,而是用ebp寄存器来读写栈内容的方法。

(3)使用[ebp+8]指定栈中存储的第1个参数123,并将其读出到eax寄存器中。像这样,不使用pop指令,也可以参照栈的内容。而之所以从多个寄存器中选择了eax寄存器,是因为eax是负责运算的累加寄存器。

通过(4)的add指令,把当前eax寄存器的值同第2个参数相加后的结果存储在eax寄存器中。[ebp+12]是用来指定第2个参数456的。在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。也就是函数的参数是通过栈来传递,返回值是通过寄存器返回的

(6)中ret指令运行后,函数返回目的地内存地址会自动出栈,据此,程序流程就会跳转返回到(6)(Call_AddNum)的下一行。这时,AddNum函数入口和出口处栈的状态变化,就如下图所示

全局变量和局部变量

在熟悉了汇编语言后,接下来我们来了解一下全局变量和局部变量,在函数外部定义的变量称为全局变量,在函数内部定义的变量称为局部变量,全局变量可以在任意函数中使用,局部变量只能在函数定义局部变量的内部使用。下面,我们就通过汇编语言来看一下全局变量和局部变量的不同之处。

下面定义的C语言代码分别定义了局部变量和全局变量,并且给各变量进行了赋值,我们先看一下源代码部分

//定义被初始化的全局变量\ninta1=1;\ninta2=2;\ninta3=3;\ninta4=4;\ninta5=5;\n\n//定义没有初始化的全局变量\nintb1,b2,b3,b4,b5;\n\n//定义函数\nvoidMyFunc(){\n//定义局部变量\nintc1,c2,c3,c4,c5,c6,c7,c8,c9,c10;\n\n//给局部变量赋值\nc1=1;\nc2=2;\nc3=3;\nc4=4;\nc5=5;\nc6=6;\nc7=7;\nc8=8;\nc9=9;\nc10=10;\n\n//把局部变量赋值给全局变量\na1=c1;\na2=c2;\na3=c3;\na4=c4;\na5=c5;\nb1=c6;\nb2=c7;\nb3=c8;\nb4=c9;\nb5=c10;\n}

上面的代码挺暴力的,不过没关系,能够便于我们分析其汇编源码就好,我们用BorlandC++编译后的汇编代码如下,编译完成后的源码比较长,这里我们只拿出来一部分作为分析使用(我们改变了一下段定义顺序,删除了部分注释)

_DATAsegmentdwordpublicuse32&39;\nalign4\n_a1labeldword\ndd1\nalign4\n_a2labeldword\ndd2\nalign4\n_a3labeldword\ndd3\nalign4\n_a4labeldword\ndd4\nalign4\n_a5labeldword\ndd5\n_DATAends\n\n_BSSsegmentdwordpublicuse32&39;\nalign4\n_b1labeldword\ndb4dup(?)\nalign4\n_b2labeldword\ndb4dup(?)\nalign4\n_b3labeldword\ndb4dup(?)\nalign4\n_b4labeldword\ndb4dup(?)\nalign4\n_b5labeldword\ndb4dup(?)\n_BSSends\n\n_TEXTsegmentdwordpublicuse32&39;\n_MyFuncprocnear\n\npushebp\nmovebp,esp\naddesp,-20\npushebx\npushesi\nmoveax,1\nmovedx,2\nmovecx,3\nmovebx,4\nmovesi,5\nmovdwordptr[ebp-4],6\nmovdwordptr[ebp-8],7\nmovdwordptr[ebp-12],8\nmovdwordptr[ebp-16],9\nmovdwordptr[ebp-20],10\nmovdwordptr[_a1],eax\nmovdwordptr[_a2],edx\nmovdwordptr[_a3],ecx\nmovdwordptr[_a4],ebx\nmovdwordptr[_a5],esi\nmoveax,dwordptr[ebp-4]\nmovdwordptr[_b1],eax\nmovedx,dwordptr[ebp-8]\nmovdwordptr[_b2],edx\nmovecx,dwordptr[ebp-12]\nmovdwordptr[_b3],ecx\nmoveax,dwordptr[ebp-16]\nmovdwordptr[_b4],eax\nmovedx,dwordptr[ebp-20]\nmovdwordptr[_b5],edx\npopesi\npopebx\nmovesp,ebp\npopebp\nret\n\n_MyFuncendp\n_TEXTends

编译后的程序,会被归类到名为段定义的组。

**初始化的全局变量,会汇总到名为_DATA的段定义中**

_DATAsegmentdwordpublicuse32&39;\n…\n_DATAends

**没有初始化的全局变量,会汇总到名为_BSS的段定义中**

_BSSsegmentdwordpublicuse32&39;\n…\n_BSSends

**被段定义_TEXT围起来的汇编代码则是BorlandC++的定义**

_TEXTsegmentdwordpublicuse32&39;\n_MyFuncprocnear\n…\n_MyFuncendp\n_TEXTends

我们在分析上面汇编代码之前,先来认识一下更多的汇编指令,此表是对上面部分操作码及其功能的接续

操作码操作数功能addA,B把A和B的值相加,并把结果赋值给AcallA调用函数AcmpA,B对A和B进行比较,比较结果会自动存入标志寄存器中incA对A的值+1ige标签名和cmp命令组合使用。跳转到标签行jl标签名和cmp命令组合使用。跳转到标签行jle标签名和cmp命令组合使用。跳转到标签行jmp标签名和cmp命令组合使用。跳转到标签行movA,B把B的值赋给ApopA从栈中读取数值并存入ApushA把A的值存入栈中ret无将处理返回到调用源xorA,BA和B的位进行亦或比较,并将结果存入A中

我们首先来看一下_DATA段定义的内容。_a1labeldword定义了_a1这个标签。标签表示的是相对于段定义起始位置的位置。由于_a1在_DATA段定义的开头位置,所以相对位置是0。_a1就相当于是全局变量a1。编译后的函数名和变量名前面会加一个(_),这也是BorlandC++的规定。dd1指的是,申请分配了4字节的内存空间,存储着1这个初始值。dd指的是definedoubleword表示有两个长度为2的字节领域(word),也就是4字节的意思。

BorlandC++中,由于int类型的长度是4字节,因此汇编器就把inta1=1变换成了_a1labeldword和dd1。同样,这里也定义了相当于全局变量的a2-a5的标签_a2-_a5,它们各自的初始值2-5也被存储在各自的4字节中。

接下来,我们来说一说_BSS段定义的内容。这里定义了相当于全局变量b1-b5的标签_b1-_b5。其中的db4dup(?)表示的是申请分配了4字节的领域,但值尚未确定(这里用?来表示)的意思。db(definebyte)表示有1个长度是1字节的内存空间。因而,db4dup(?)的情况下,就是4字节的内存空间。

注意:db4dup(?)不要和dd4混淆了,前者表示的是4个长度是1字节的内存空间。而db4表示的则是双字节(=4字节)的内存空间中存储的值是4

临时确保局部变量使用的内存空间

我们知道,局部变量是临时保存在寄存器和栈中的。函数内部利用栈进行局部变量的存储,函数调用完成后,局部变量值被销毁,但是寄存器可能用于其他目的。所以,局部变量只是函数在处理期间临时存储在寄存器和栈中的

回想一下上述代码是不是定义了10个局部变量?这是为了表示存储局部变量的不仅仅是栈,还有寄存器。为了确保c1-c10所需的域,寄存器空闲的时候就会使用寄存器,寄存器空间不足的时候就会使用栈。

让我们继续来分析上面代码的内容。_TEXT段定义表示的是MyFunc函数的范围。在MyFunc函数中定义的局部变量所需要的内存领域。会被尽可能的分配在寄存器中。大家可能认为使用高性能的寄存器来替代普通的内存是一种资源浪费,但是编译器不这么认为,只要寄存器有空间,编译器就会使用它。由于寄存器的访问速度远高于内存,所以直接访问寄存器能够高效的处理。局部变量使用寄存器,是BorlandC++编译器最优化的运行结果。

代码清单中的如下内容表示的是向寄存器中分配局部变量的部分

moveax,1\nmovedx,2\nmovecx,3\nmovebx,4\nmovesi,5

仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。上述代码相当于就是给5个局部变量c1-c5分别赋值为1-5。eax、edx、ecx、ebx、esi是x86系列32位CPU寄存器的名称。至于使用哪个寄存器,是由编译器来决定的。

x86系列CPU拥有的寄存器中,程序可以操作的是十几,其中空闲的最多会有几个。因而,局部变量超过寄存器数量的时候,可分配的寄存器就不够用了,这种情况下,编译器就会把栈派上用场,用来存储剩余的局部变量。

在上述代码这一部分,给局部变量c1-c5分配完寄存器后,可用的寄存器数量就不足了。于是,剩下的5个局部变量c6-c10就被分配给了栈的内存空间。如下面代码所示

movdwordptr[ebp-4],6\nmovdwordptr[ebp-8],7\nmovdwordptr[ebp-12],8\nmovdwordptr[ebp-16],9\nmovdwordptr[ebp-20],10

函数入口addesp,-20指的是,对栈数据存储位置的esp寄存器(栈指针)的值做减20的处理。为了确保内存变量c6-c10在栈中,就需要保留5个int类型的局部变量(4字节*5=20字节)所需的空间。movebp,esp这行指令表示的意思是将esp寄存器的值赋值到ebp寄存器。之所以需要这么处理,是为了通过在函数出口处movespebp这一处理,把esp寄存器的值还原到原始状态,从而对申请分配的栈空间进行释放,这时栈中用到的局部变量就消失了。这也是栈的清理处理。在使用寄存器的情况下,局部变量则会在寄存器被用于其他用途时自动消失,如下图所示。

movdwordptr[ebp-4],6\nmovdwordptr[ebp-8],7\nmovdwordptr[ebp-12],8\nmovdwordptr[ebp-16],9\nmovdwordptr[ebp-20],10

这五行代码是往栈空间代入数值的部分,由于在向栈申请内存空间前,借助了movebp,esp这个处理,esp寄存器的值被保存到了esp寄存器中,因此,通过使用[ebp-4]、[ebp-8]、[ebp-12]、[ebp-16]、[ebp-20]这样的形式,就可以申请分配20字节的栈内存空间切分成5个长度为4字节的空间来使用。例如,movdwordptr[ebp-4],6表示的就是,从申请分配的内存空间的下端(ebp寄存器指示的位置)开始向前4字节的地址([ebp-4])中,存储着6这一4字节数据。

循环控制语句的处理

上面说的都是顺序流程,那么现在就让我们分析一下循环流程的处理,看一下for循环以及if条件分支等c语言程序的流程控制是如何实现的,我们还是以代码以及编译后的结果为例,看一下程序控制流程的处理过程。

//定义MySub函数\nvoidMySub(){\n//不做任何处理\n\n}\n\n//定义MyFunc函数\nvoidMyfunc(){\ninti;\nfor(inti=0;i<10;i++){\n//重复调用MySub十次\nMySub();\n}\n}

上述代码将局部变量i作为循环条件,循环调用十次MySub函数,下面是它主要的汇编代码

xorebx,ebx;将寄存器清0\n@4call_MySub;调用MySub函数\nincebx;ebx寄存器的值+1\ncmpebx,10;将ebx寄存器的值和10进行比较\njlshort@4;如果小于10就跳转到@4

C语言中的for语句是通过在括号中指定循环计数器的初始值(i=0)、循环的继续条件(i<10)、循环计数器的更新(i++)这三种形式来进行循环处理的。与此相对的汇编代码就是通过比较指令(cmp)和跳转指令(jl)来实现的。

下面我们来对上述代码进行说明

MyFunc函数中用到的局部变量只有i,变量i申请分配了ebx寄存器的内存空间。for语句括号中的i=0被转换为xorebx,ebx这一处理,xor指令会对左起第一个操作数和右起第二个操作数进行XOR运算,然后把结果存储在第一个操作数中。由于这里把第一个操作数和第二个操作数都指定为了ebx,因此就变成了对相同数值的XOR运算。也就是说不管当前寄存器的值是什么,最终的结果都是0。类似的,我们使用movebx,0也能得到相同的结果,但是xor指令的处理速度更快,而且编译器也会启动最优化功能。

XOR指的就是异或操作,它的运算规则是如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0

相同数值进行XOR运算,运算结果为0。XOR的运算规则是,值不同时结果为1,值相同时结果为0。例如01010101和01010101进行运算,就会分别对各个数字位进行XOR运算。因为每个数字位都相同,所以运算结果为0。

ebx寄存器的值初始化后,会通过call指定调用_MySub函数,从_MySub函数返回后,会执行incebx指令,对ebx的值进行+1操作,这个操作就相当于i++的意思,++表示的就是当前数值+1。

这里需要知道i++和++i的区别

i++是先赋值,复制完成后再对i执行+1操作

++i是先进行+1操作,完成后再进行赋值

inc下一行的cmp是用来对第一个操作数和第二个操作数的数值进行比较的指令。cmpebx,10就相当于C语言中的i<10这一处理,意思是把ebx寄存器的值与10进行比较。汇编语言中比较指令的结果,会存储在CPU的标志寄存器中。不过,标志寄存器的值,程序是无法直接参考的。那如何判断比较结果呢?

汇编语言中有多个跳转指令,这些跳转指令会根据标志寄存器的值来判断是否进行跳转操作,例如最后一行的jl,它会根据cmpebx,10指令所存储在标志寄存器中的值来判断是否跳转,jl这条指令表示的就是jumponlessthan(小于的话就跳转)。发现如果i比10小,就会跳转到@4所在的指令处继续执行。

那么汇编代码的意思也可以用C语言来改写一下,加深理解

i^=i;\nL4:MySub();\ni++;\nif(i<10)gotoL4;

代码第一行i^=i指的就是i和i进行异或运算,也就是XOR运算,MySub()函数用L4标签来替代,然后进行i自增操作,如果i的值小于10的话,就会一直循环MySub()函数。

条件分支的处理方法

条件分支的处理方式和循环的处理方式很相似,使用的也是cmp指令和跳转指令。下面是用C语言编写的条件分支的代码

//定义MySub1函数\nvoidMySub1(){\n\n//不做任何处理\n}\n\n//定义MySub2函数\nvoidMySub2(){\n\n//不做任何处理\n}\n\n//定义MySub3函数\nvoidMySub3(){\n\n//不做任何处理\n}\n\n//定义MyFunc函数\nvoidMyFunc(){\n\ninta=123;\n//根据条件调用不同的函数\nif(a>100){\nMySub1();\n}\nelseif(a<50){\nMySub2();\n}\nelse\n{\nMySub3();\n}\n\n}

很简单的一个实现了条件判断的C语言代码,那么我们把它用BorlandC++编译之后的结果如下

_MyFuncprocnear\npushebp\nmovebp,esp\nmoveax,123;把123存入eax寄存器中\ncmpeax,100;把eax寄存器的值同100进行比较\njleshort@8;比100小时,跳转到@8标签\ncall_MySub1;调用MySub1函数\njmpshort@11;跳转到@11标签\n@8:\ncmpeax,50;把eax寄存器的值同50进行比较\njgeshort@10;比50大时,跳转到@10标签\ncall_MySub2;调用MySub2函数\njmpshort@11;跳转到@11标签\n@10:\ncall_MySub3;调用MySub3函数\n@11:\npopebp\nret\n_MyFuncendp

上面代码用到了三种跳转指令,分别是jle(jumponlessorequal)比较结果小时跳转,jge(jumpongreaterorequal)比较结果大时跳转,还有不管结果怎样都会进行跳转的jmp,在这些跳转指令之前还有用来比较的指令cmp,构成了上述汇编代码的主要逻辑形式。

了解程序运行逻辑的必要性

通过对上述汇编代码和C语言源代码进行比较,想必大家对程序的运行方式有了新的理解,而且,从汇编源代码中获取的知识,也有助于了解Java等高级语言的特性,比如Java中就有native关键字修饰的变量,那么这个变量的底层就是使用C语言编写的,还有一些Java中的语法糖只有通过汇编代码才能知道其运行逻辑。在某些情况下,对于查找bug的原因也是有帮助的。

上面我们了解到的编程方式都是串行处理的,那么串行处理有什么特点呢?

串行处理最大的一个特点就是专心只做一件事情,一件事情做完之后才会去做另外一件事情。

计算机是支持多线程的,多线程的核心就是CPU切换,如下图所示

我们还是举个实际的例子,让我们来看一段代码

//定义全局变量\nintcounter=100;\n\n//定义MyFunc1()\nvoidMyFunc(){\ncounter*=2;\n}\n\n//定义MyFunc2()\nvoidMyFunc2(){\ncounter*=2;\n}

上述代码是更新counter的值的C语言程序,MyFunc1()和MyFunc2()的处理内容都是把counter的值扩大至原来的二倍,然后再把counter的值赋值给counter。这里,我们假设使用多线程处理,同时调用了一次MyFunc1和MyFunc2函数,这时,全局变量counter的值,理应编程100*2*2=400。如果你开启了多个线程的话,你会发现counter的数值有时也是200,对于为什么出现这种情况,如果你不了解程序的运行方式,是很难找到原因的。

我们将上面的代码转换成汇编语言的代码如下

moveax,dwordptr[_counter];将counter的值读入eax寄存器\naddeax,eax;将eax寄存器的值扩大2倍。\nmovdwordptr[_counter],eax;将eax寄存器的值存入counter中。

在多线程程序中,用汇编语言表示的代码每运行一行,处理都有可能切换到其他线程中。因而,假设MyFun1函数在读出counter数值100后,还未来得及将它的二倍值200写入counter时,正巧MyFun2函数读出了counter的值100,那么结果就将变为200。

为了避免该bug,我们可以采用以函数或C语言代码的行为单位来禁止线程切换的锁定方法,或者使用某种线程安全的方式来避免该问题的出现。

现在基本上没有人用汇编语言来编写程序了,因为C、Java等高级语言的效率要比汇编语言快很多。不过,汇编语言的经验还是很重要的,通过借助汇编语言,我们可以更好的了解计算机运行机制。

附7个程序员接私活平台

挺多程序员朋友应该都有这个想法,但是苦于找不到途径,今天给大家带来7大平台,稍后介绍。先来分析一下,有啥接私活有什么要注意的事项。

接私活的注意事项

1、需求要整明白

不像在公司里干活,你只是一个程序员,你上面有业务部与外面对接,你只需要按需求做就行了,这里就不一样,你不单单是程序员角色,你还有业务的角色在这里。

还有就是避免以后有什么纠纷,或者后续提出更改需求(这个提前说好,改需求双方如何处理,你可以要求对方支付额外费用,比较更改需求也会提高你的成本),因此这点需要特别注意。

2、保持沟通

同时,在做项目的过程中对自己的一些想法一定要随时保持沟通,这些做法都是为了防止需求跑偏,避免造成没必要的麻烦。

3、工期要整明白

如果需求内容沟通明白了,项目的截止日期也是相对比较重要的,首先不要拍脑袋直接给对方日期。也要给自己留一些余地,不要把时间排的满满的,万一家里有什么意外,比如亲戚来访什么的话,很容易打破你的计划,也会导致你实现不了你的承诺。

4、不要在上班时间去做

如果是自己接私活的话,最好不要选择上班时间去做,即使你手头的事情都做完了,也不要去做。因为被公司发现了,那将是非常不好的,会影响自己的前途,在这方面要做好自己的管理与规划。

接完私活的我

接私活的平台

1、程序员客栈:程序员的经纪人

2、快码众包-让互联网产品开发更快速!认准域名Kuai.ma

3、Coding码市-基于云技术的软件外包服务平台coding码市

4、开源中国众包平台oschina众包

5、码易-高质量软件众包交付服务平台码易众包平台

6、人人开发-集可视化开发,应用市场,威客众包,PaaS云于一体的企业级应用服务平台人人开发

7、开发邦-专业互联网软件技术开发与咨询服务开发邦

写在最后

接私活会占用你大量的业余时间,所以是否应该接私活,自己一定要慎重考虑一下,毕竟,业余时间也是提升你自己的时间。

也不是说不要接,可以尽可能的接一些能对自己有提升的项目,比如该项目中大部分东西你都没问题,但是小部分技术,还需要自己去学习一下,才可以搞定。

这种项目其实是最好的,既赚了外快,也提升了自己。

不建议接那种对自己没有挑战的私活,可以把业余时间放到提升自己技术上面,从而为以后找到更好的工作。

好了,本文到此结束,如果可以帮助到大家,还望关注本站哦!

Published by

风君子

独自遨游何稽首 揭天掀地慰生平