这让我感到困惑的原因是所有地址都包含一个1和0的序列。那么CPU是如何区分00000100
(整数)和00000100
(CPU指令)的呢?
首先,不同的命令具有不同的值(操作码)。这就是CPU知道该做什么的方式。
最后,问题仍然存在:什么是命令,什么是数据?
现代PC正在使用von Neumann-体系结构(https://en.wikipedia.org/wiki/John_von_Neumann)其中数据和操作码存储在相同的存储器空间中。(有一些体系结构将这两种数据类型分开,例如哈佛体系结构)
详细解释所有内容完全超出了stackoverflow的范围,很可能每条帖子的字符数量不够。
用尽可能少的单词回答这个问题(每个真正在这个级别上工作的人都会因为解释中的捷径而杀了我):
- 存储器中的数据存储在特定地址
- 每个CPU建议基本上由3个不同的地址组成(不是值,只是地址!):
- Adress关于该做什么
- 关于值的Adress
- Adress about一个附加值
因此,假设应该执行加法,并且内存中有3个可用的Adress,应用程序将存储(在5+7
的情况下)(我在指令中使用了"动词")
Adress | Stored Value
1 | ADD
2 | 5
3 | 7
最后,CPU接收指令1 2 3
,这意味着ADD 5 7
(这些东西是顺序敏感的![Command][v1][v2])。。。现在事情变得越来越复杂。
CPU会将这些值(实际上不是值,只是值的地址)移动到其寄存器中,然后进行处理。要选择的确切寄存器取决于数据类型、数据大小和操作码。
在命令#1 #2 #3
的情况下,CPU将首先读取这些存储器地址,然后知道需要ADD 5 7
。
根据ADD
的操作码,CPU将知道:
- 将地址
#2
放入r1 - 将地址
#3
放入r2 - 读取存储在r1中存储地址的内存值
- 读取存储在r2中存储地址的内存值
- 将两个值相加
- 将结果写入内存中的某个位置
- 将结果放入r3的存储地址
- 将存储在r3中的地址存储到存储在r1中的内存地址中
请注意这是简化的。实际上,CPU需要确切的指令来确定它是处理值还是地址。在组装中,这是通过使用来完成的
- eax(表示存储在寄存器eax中的值)
- [eax](表示存储在寄存器eax中地址的存储器中的值)
CPU无法对存储在内存中的值进行计算,因此它忙于将值从内存移动到寄存器,从寄存器移动到内存。
即,如果您有
eax = 0x2
和内存
0x2 = 110011
和指令
MOV ebx, [eax]
这意味着:将当前存储在eax
中的地址处的值移动到寄存器ebx
中。所以最后
ebx = 110011
(这种情况总是发生在CPU进行单个计算的时候!.Memory->Register->Memory)
最后要求苛刻的应用程序可以读取其预定义的存储器地址#2,从而得到地址#2568并且然后知道计算结果存储在地址#2658。读取该Adress将导致值12
(5+7)
这只是正在发生的事情的一个很小的例子。有关这方面的更详细介绍,请参阅http://www.cs.virginia.edu/~evans/cs216/guides/x86.html
人们无法真正掌握为简单添加2个值所做的数据移动和计算量。做CPU所做的事情(在纸上)只需要几分钟就可以计算出"5+7",因为没有"5"one_answers"7"-所有东西都隐藏在内存中的地址后面,指向一些位,根据地址0x1的位指示的内容产生不同的值。。。
缩写:CPU不知道那里存储了什么,但指令告诉CPU如何解释它。
让我们举一个简化的例子。
如果CPU被告知添加存储在位置X的一个字(比方说,一个32位整数),它就会获取该地址的内容并添加它
如果程序计数器到达相同的位置,CPU将再次获取该字并将其作为命令执行。
CPU(除了NX位之类的安全性东西)对数据还是代码都是盲目的。
数据不会意外地作为代码执行的唯一方法是仔细组织代码,使其永远不会引用包含数据的位置和用于操作代码的指令。
当程序启动时,处理器会在预定义的位置开始执行程序。用机器语言编写的程序的作者会有意将程序的开头放在那里。从那里开始,该指令将始终设置处理器将执行的下一个位置——这是一条指令。除非代码中有严重的错误,否则组成程序的所有指令都是如此。
指令可以通过两种主要方式设置处理器下一步的位置:跳转/分支,以及不显式指定。如果指令没有明确指定下一步的位置,则CPU默认为当前指令后面的位置。相比之下,跳转和分支有空间专门编码下一条指令的地址。跳跃总是跳到指定的位置。分支检查条件是否为真。如果是,CPU将跳转到编码位置。如果条件为false,它将直接转到分支之后的指令。
此外,机器语言程序不应该将数据写入用于指令的位置,或者在程序的某个未来点,其他指令可能会尝试运行被数据覆盖的内容。发生这种情况可能会导致各种各样的坏事发生。那里的数据可能有一个"操作码",与处理器知道要做的任何事情都不匹配。或者,那里的数据可以告诉计算机做一些完全出乎意料的事情。不管怎样,你都会有糟糕的一天。很高兴你的编译器从来不会搞砸,也不会意外地插入这样的东西。
不幸的是,有时使用编译器的程序员会搞砸,并做一些事情,告诉CPU在分配给数据的区域之外写入数据。(在C/C++中发生这种情况的一种常见方式是将数组分配为长L项,并在写入数据时使用索引>=L。)将数据写入为代码留出的区域是缓冲区溢出漏洞的原因。有些程序可能有一个错误,让远程机器欺骗程序将数据(远程机器发送的)写入到为数据留出的区域的末尾之外,并写入为代码留出的区域。然后,在稍后的某个时刻,处理器执行该"数据"(记住,该数据是从远程计算机发送的)。如果远程计算机/攻击者很聪明,他们会精心制作越过边界的"数据",使其成为进行恶意操作的有效指令。(为了给他们更多的访问权限,可以销毁数据、从内存发回敏感数据等)。
这是因为ISA必须考虑什么是有效的指令集以及如何对数据进行编码:内存地址/寄存器/文字。
有关如何设计ISA的更多一般信息,请参阅此https://en.wikipedia.org/wiki/Instruction_set
简言之,操作系统"告诉"它下一条指令在哪里。在x64的情况下,有一个名为rip(指令指针)的特殊寄存器,它保存要执行的下一个指令的地址。它将自动读取该地址的数据,对其进行解码和执行,并自动将rip增加指令的字节数。
通常,OS可以将存储器(页面)的区域标记为保存或不保存可执行代码。如果错误或利用漏洞试图修改可执行内存,则应该发生错误,类似地,如果CPU发现自己试图执行不可执行内存时,它也会/应该发出错误信号并终止程序。现在你进入了软件病毒的奇妙世界!