Modern program and memory structure
About articles in old-blogpost: These pages are old and migrated in gitbook without translation. Probably I will translate them in the future.
Last updated
About articles in old-blogpost: These pages are old and migrated in gitbook without translation. Probably I will translate them in the future.
Last updated
我们了解到,在操作系统中,我们双击一个程序,便会使程序运行。那么在这个过程中,操作系统做了什么呢?首先让我们了解操作系统的工作流程。根据维基百科,操作系统的定义是:
操作系统(英語:Operating System,縮寫:OS)是一组主管并控制计算机操作、运用和运行硬件、软件資源和提供公共服务来组织用户交互的相互关联的系统软件程序,同时也是计算机系统的核心与基石。操作系统需要处理如管理與配置内存、決定系統資源供需的優先次序、控制輸入與輸出裝置、操作网络與管理文件系统等基本事務。
总结来说,操作系统在我们的电脑中主要在做这两方面的工作:硬件管理和应用程序管理。硬件管理是操作系统的底层工作,控制CPU、内存、硬盘等硬件设备。应用程序管理则是我们熟悉的程序运行,目前阶段,我们将重点放在应用程序管理方面。
以Windows系统为例,当我们双击运行一个程序时,操作系统会将整个程序拷贝到内存中,包含程序内部的数据和程序的机器码。我们要做的就是修改内存中的程序,从而使程序达到我们想要的功能。
内存,在硬件中称作随机存取存储器,缩写为RAM。是电脑中的硬件,如其名,它的用途自然是随机存取,随机存取指的是写入时的时间与要写入的位置无关,如此一来,内存便有了较高的存取速度。所以,比起在随机存取性能较差的硬盘中执行程序,不如将程序拷贝到内存中执行,这样可以显著提高程序执行效率。
众所周知,我们需要电力来运行计算机(至少在2020年是这样),由此可推论,内存也需要靠电力存储数据。我们用电力的通断表示0和1,从而可以存储二进制数据。在操作系统中,我们的内存以8个存储单元为一组,所以一组内存的存储最大值为 $2^8 = 256$。这一组内存称作一字节(Byte),这是我们在操作系统中能访问的最基本单位。
那么为什么我们要定义8位为一组呢?如果我们使用调试器进行内存数据的读取便会知道,我们所在的内存各种数据参差不齐,如果我们以二进制的方式进行表示,那么便会是下图这样。
这样的数据看起来很复杂,而且程序员需要经过大量的训练才能快速理解二进制数据,这样对程序员非常不友好。于是我们的天才计算机科学家定义4位为一组,使用16进制正好可以用0x0~0xF(0x为16进制标志,用于表示该值是16进制数)表示0000~1111的数值,同时可以使程序员更好理解,也使数据更加整齐易读。同时,为了加大范围,我们的天才计算机科学家便用2个16进制数表示1字节,例如0xFF的二进制是11111111,即为 256。如此一来,阅读便会简单无数倍。
P.S. 还有8进制(00位前缀)的表示方法,这种表示方法是三位为一组,过程类似,不在此多加赘述。
程序在存储数据时首先定义数据类型,然后在对相应内存的区块进行写入。那么程序找到这个区块的位置呢?实际上操作系统已经将内存的区块位置用数值的方式表示出来了,这个数值的名称为内存地址。举个例子,如果我要去串门,我需要知道我想要拜访的人的门牌号,才能找到这个人,内存地址就像门牌号一样,有了内存地址才能找到数据所存放的位置。
有符号整型(signed int)将最高比特位作为补码位(用于识别数值的正负),所以其将最高比特位(内存中表示最大数目单位位置,例如在十进制中,400的最高位为'4')设置为类似标记位的补码,用于识别数值正负,所以无符号整型的取值范围是
-2^{8\*4-1} \sim 2^{8\*4-1}-1 = -2147483648 \sim 2147483647
P.S. 在正数最大值处-1是为了排除0,因为该计算的基本方式是数学中的总样本计算方法,而不是进制转换,需要剔除重复部分,若使用Windows自带的计算器进行计算可以通过进制转换的方式求得以上范围。
总结来说,例如我们要取得内存地址为0x1FF的值,那么我们首先需要得知其数据类型,找到0x1FF的内存位置,再通过数据类型得到存储数据的区块,通过读取该区块进行数据的读取,再让程序执行下一步操作。
注:此处的数据是软件层面上的数据,指的是程序意义上的数据。若是将定义范围放至内存中,所有在内存中的数据,包括机器码,都叫做数据。所以我们的数据、机器码仅仅是一段内存中存放的数值,随时可以被修改。
上文已经提到,我们所修改的是程序在内存中的拷贝。程序在内存中又包含数据和机器码,我们两者皆可修改,那么数据和机器码具体代表了什么?我们打个比方,我们有100元钱,我们去店铺买了10元的彩票,现在还剩90元。开奖后发现彩票中奖,兑奖后得到300元,所以我们现在有390元。那么数据就是我们所有的钱数,而买彩票、兑奖等操作则是由机器码控制。
机器码是可以由CPU直接执行的二进制数据。学过编程的朋友应该知道,编译型语言编译时会将高级语言编译成汇编语言,汇编语言是直接对硬件、内存操作的一条条指令,而汇编语言CPU也是无法直接执行的,因为汇编语言也是人类为方便阅读所发明出来的语言,CPU不会像人类一样识别文本,所以汇编语言会被编译成机器码。因此,机器码是最底层的、可被直接执行的一组数值。
举个例子,在x86指令集的CPU中,jmp操作所对应的操作码为E9,以0x89AB为操作数,则指令为jmp 0x89AB
。设当前EIP为0,则jmp 0x89AB的机器码为 E9 00 00 89 AB。(E9后的数值为DWORD类型的相对地址,指的是跳转的距离,例如程序想从0x89AB跳转到0x9000,则机器码为E9 00 00 06 55)
看完上述段落,相信你也了解了程序在内存中的结构,我们需要注意的是程序在执行过程中可以申请更多的内存,所以内存的结构并不是一成不变的(包括机器码也有可能在运行过程中改变,例如提取的shellcode)。那么我们的程序在拷贝到内存后,首先从入口点(第一条指令位置)开始,由上到下一条条指令执行,执行完毕后由程序和操作系统清理内存,退出进程。整体流程如下:
了解内存结构和程序执行流程后,接下来对内存修改技术的理解就会变得容易,我们将会在下一节了解基本寻址方法和指针。
学过编程的朋友应该知道,在C语言中有许多数据类型,例如int
double
float
等,这些类型都是“多字节存储”,意思是使用多个字节进行数据存储,我们以32位Windows操作系统为例,在该操作系统中,无符号整型(unsigned int)占4字节,那么它的存储范围为