不受限制的低级硬件编程和实验平台(NakedCPU)
本文介绍了一个由两台计算机组成的实验平台,用于在不受任何操作系统限制的情况下进行硬件编程和研究。
前言
如今,硬件被厚厚的操作系统代码层所掩盖。然而,对我以及根据 Google 搜索结果显示,对许多其他人来说,能够接触并实验硬件将是一个非常令人兴奋的机会。我创建了一个由两台基于 Intel 的计算机通过 RS232 接口连接的探索平台。一台计算机(主计算机)运行 Windows,用于控制第二台计算机。第二台计算机(NakedCPU)基本上没有任何操作系统,因此可以用于真正的底层实验。我的平台曾刊登在《Circuit Cellar Magazine》第259、260期封面上。本文是原始出版物的改编版,可在我的网站上查阅。
引言
看来,PC 的实验仅限于借助大量库和技术开发高级代码软件,这些库和技术将硬件隐藏在层层代码之下。很少能对 PC 硬件进行有限的实验,但是必须安装允许访问硬件的驱动程序,因为操作系统自然不允许我们进行任何底层活动。遗憾的是,这些驱动程序本身就很神秘。可以肯定地说,硬件编程在 80 年代为许多计算机专业人士和爱好者所熟知。后来人们忘记了它,而技术却突飞猛进。在本文中,我尝试弥合时间的鸿沟,并基于最先进的技术和概念重燃对硬件编程的兴趣。俄罗斯有句谚语:“一切新事物实际上都是被遗忘已久的旧事物。”
本文是我对 Intel CPU、芯片组、I/O 控制器和其他基本 PC 设备从不受操作系统和驱动程序遮蔽的底层硬件编程角度感兴趣的结果。该项目的动机是接触那些具有好奇心并会欣赏直接实验 CPU、芯片组和其他硬件的可能性的人。在此,我介绍 NakedCPU:一个提供对硬件和 CPU 完全访问的设施,不受操作系统强加的任何限制。重要的是,处理器不会被 Linux、DOS 或 Windows 遮蔽,它将以其最有趣和最强大的模式运行——保护模式。在文本中,用户被称为探究者,因为 NakedCPU 是为研究人员(即忠实的极客)而非普通用户而设计的。
本文的另一个目标是为探究者提供一个导航硬件文档的路线图,否则这些文档令人困惑且难以查找。我不想重述文档,因为许多计算机概念和技术很快就会过时。然而,有了路线图,将更容易跟踪更新的技术和文档。
有趣的事实
让我们花点时间思考一下现代 Intel CPU 的一个变种,例如 Intel Core 2 Duo。令人印象深刻的是,这款处理器能够消耗高达 75A 的电流 [1]!此外,它不是一个简单的处理器:其文档由 5 卷组成,总页数约为 4200 页 [2]。Intel CPU 并非单独运行:它与芯片组(即图形和内存控制器集线器 (GMCH))接口。芯片组的另一端连接到 I/O 控制器集线器 (ICH)。有趣的是,这种安排类似于我们的大脑、脑干和脊髓组成的神经系统。GMCH 和 ICH 本身就是处理器,包含数百个配置和控制寄存器。GMCH 和 ICH 的文档超过 1400 页 [3, 4]。难怪操作系统会将实际硬件隐藏在厚厚的中间代码层之下!
NakedCPU 的工作原理
NakedCPU 是一个揭示 PC 硬件内部结构的实验平台。使用 NakedCPU 进行实验需要两台计算机(图 1)。一台是主计算机,运行 Windows 和 Visual Studio 软件,其任务是与我们和第二台计算机交互。第二台计算机(即 NakedCPU)通过 RS232 接口连接到主计算机。NakedCPU 计算机通过一小段启动代码(此处提供)启动,使其能够通过 RS232 与主计算机通信。启动后,NakedCPU 期望接收两个独立的字节包:一个是待执行的 Intel CPU 操作码流(即可执行文件),另一个是待处理的数据。可执行文件可以修改内存的任何部分、芯片组寄存器等,甚至覆盖启动代码。换句话说,自由由您掌控。
启动 NakedCPU
NakedCPU 离开某种启动代码将无法运行。启动时,我们必须完成两项任务:将 CPU 切换到保护模式,并开始监听串口以接收两个字节包:可执行文件和数据。为了向 NakedCPU 提供启动代码,最简单的方法是准备一个带有我们自己代码的可引导软盘。当然,也可以将此代码放入硬盘。启动代码(最多 512 字节)用汇编语言编写,并且必须存储在磁盘的扇区 0,即主引导记录 (MBR) 中。使用 MASM 等汇编语言编译器和链接器很困难,因为它们会根据特定的操作系统裁剪可执行文件。但是,有一个二进制编辑器 HexIt [5],它除其他功能外,还允许将汇编命令直接转换为二进制代码。使用此编辑器,创建了未来 MBR 的二进制文件。此文件的内容可以在附录中查看。“MBR 的剖析”提供了内容的详细分解。一个小工具“Firstsectwrite.exe”(参见附录)被编写用于将此文件传输到磁盘的扇区 0。尽管此工具的代码非常简单,但它值得关注。一个 Windows API 调用
CreateFile(TEXT("\\\\.\\A:")...)
打开与磁盘的原始通信,在本例中是软盘驱动器 A,以允许写入扇区 0。需要注意的是,此调用只有在管理员帐户下才能成功。本文中使用戴尔 Optiplex 760 计算机进行实验。它有一个通过 USB 连接的软盘驱动器,BIOS 启动选项允许从该驱动器启动计算机。
本文中使用戴尔 Optiplex 760 计算机进行实验。它有一个通过 USB 连接的软盘驱动器,BIOS 启动选项允许从该驱动器启动计算机。
NakedOS
听起来与本文的无操作系统精神相矛盾,但 NakedCPU 是通过一个微小的(262字节长)32位“操作系统”NakedOS 启动的,这使得 NakedCPU 能够通过串口与外界通信。事实上,我们并没有违背真正自由探索的原则,因为 NakedOS 是绝对透明的,其代码在附录中完整呈现。NakedOS 定义了几个内存段(表1),这些段作为探究者可执行文件的初始环境很有用。Intel 文档[2]提供了对保护模式内存段、全局描述符表(GDT)和中断描述符表(IDT)的解释。此外,NakedOS 定义了两个软件中断和一个硬件中断基向量。注意:INT 中断与 DOS 或 BIOS 无关;它们完全由我们的代码定义。
Segment | Base | 大小 | 描述符,类型 |
扩展内存 | 0x100000 | ~ 128MB | 0x28, 数据 |
屏幕,字符模式 | 0x0B8000 | 4KB | 0x20, 数据 |
目标可执行文件 | 0x93B | 64KB | 0x30, 代码 32;0x38, 数据 |
NakedOS | 0x800 | 315 字节 | 0x10, 代码 32 |
堆栈 | 0x400 | 1024 字节 | 0x18, 堆栈 32 |
系统数据 IDT: 0x3FF-0x200 GDT: 0x1FF-0 |
0x0 | 1024 字节 | 0x8, 数据 |
中断 | Info(信息) |
INT 0x20 | 从串口读取一个数据包;目的地 ES:[EDI];强制条件 DS=ES。数据包的前4个字节表示后续字符串的长度(以字节为单位)。返回时,ECX 包含接收到的字节数。 |
INT 0x21 | 向串口发送一个位于 DS:[ESI] 的 ECX 字节字符串。 |
IRQ0 | 硬件中断基向量为 0x28。 |
启动后,NakedOS 立即期待两次事务:一次用于可执行代码,另一次用于数据。每个事务都是通过 RS232 发送的字节流(参见图 2)。第一次事务写入“目标可执行文件”内存段,而第二次事务写入“扩展内存”段。第二次事务完成后,NakedOS 通过一个长跳转将控制权转移给可执行文件
jmp 00030:000000000
从那时起,原则上,NakedOS 占用的任何内存都可以被探究者可执行文件的活动覆盖。当 NakedOS 运行时,硬件中断通常被屏蔽;但是,如果探究者决定解除屏蔽,则会设置 8259 中断控制器(参见下载存档中的附加文件 MBRListingNakedOS.doc)来处理中断。有关中断控制器编程的详细说明可在 I/O 控制器集线器 (ICH) 文档 [4] 中找到。
NakedCPU Explorer 和其他可执行文件
如何向 NakedCPU 发送可执行代码以进行实验仍然是一个重要问题。回想文章开头,其中提到涉及两台计算机。主计算机有一个 Visual C++ 项目 NakedCPU Explorer,它充当一个“shell”,允许检查和修改芯片组寄存器和内存。代码定义了一个类,该类有一个构造函数,它提供了 __asm{}
括号供探究者填充可执行代码
class Ports : public NakedCPUcode
{
public:
Ports()
{
DWORD pe, ps;
__asm
{
mov pe, offset end //save end and start of the code
mov ps, offset start //to be sent to the NakedCPU
jmp end //master jumps over the NakedCPU code
///////////////////////////////////////////
start: mov ax, 0x28 //loading
mov es, ax //data
mov ds, ax //and stack
mov ax, 0x18 //segment registers
mov ss, ax //initializing
mov esp, 0x3fe //stack pointer
xor edi, edi
mov eax, 'OLEH' //NakedCPU Explorer says HELO
stosd
xor esi, esi
mov ecx, 4
INT 0x21
... //here goes rest of the code
_emit 0xEA
_emit 0x00
_emit 0x00
_emit 0x00
_emit 0x00
_emit 0x10
_emit 0x00
end: nop
}
if(!PrepareCode(ps, pe)) delete this;
}
};
由于 Microsoft Visual C++ 运行在拥有 Intel CPU 的主 PC 上,编译器会将汇编代码翻译成适当的操作码,这些操作码自然适用于 NakedCPU!具体来说,这个类派生自另一个类 NakedCPUcode,NakedCPUcode 通过从 __asm{}
括号中的代码中提取生成的操作码,并使其可用于发送到 NakedCPU 来执行准备工作。请注意,NakedCPU 只接收“start”和“end”标签之间的代码。重要的是要理解,主计算机不会执行 __asm{}
括号中的代码,它只是跳过它。奇怪的关键字“_emit”允许直接通过其十六进制值放置操作码——出于某种原因,在使用 Visual Studio 编译器时不允许长跳转。
除了 NakedCPU Explorer 之外,任何其他可执行代码都可以准备并发送到 NakedCPU 计算机。NakedCPU Explorer 代码的实际发送通过以下方式完成
Ports ncd;
SerialComm Com1;
if(!ncd.UploadEx(&Com1))
throw 1;
该项目还定义了一个类 SerialComm 和一个函数 SendNakedCPUdataRecvResponse 来发送和接收数据。仔细研究项目中的简单代码有助于理解与 NakedCPU 通信的细节。除了作为示例外,NakedCPU Explorer 还向 NakedCPU 发送一个可执行文件,该文件允许交互式检查和修改各种芯片组和 I/O 控制器寄存器。NakedCPU Explorer 提供八个命令:“write”、“write32”、“read”、“read32”、“pci”、“memread”、“memwrite”和“quit”。前四个命令将询问端口地址,即 CPU I/O 空间中的地址。通过这些命令,NakedCPU 将向 GMCH 或 ICH 寄存器写入和读取一个或四个字节。第五个命令将询问总线 (十进制)、设备 (十进制)、功能 (十进制) 和寄存器 (十六进制) 值。这些值将被打包到端口 0xCF8 中,以打开一个 PCI 配置空间的“窗口”,该窗口可通过端口 0xCFC 访问。有关 PCI 设备寻址的详细信息可在芯片组文档 [3] 中找到。Memread 和 memwrite 允许分别从内存中读取和写入双字。
注意:NakedCPU Explorer 不使用任何隐藏的“辅助”驱动程序或库。代码小巧且对查询者完全透明。
实验
以下部分描述了直接访问硬件和 CPU 的实验。
发出噪音
虽然听起来微不足道,但让 PC 扬声器发出声音涉及理解计时器和一些底层工作。讽刺的是,在 Vista 和 XP 64 位版本上似乎无法使用 Windows API 让扬声器发出蜂鸣声,因为微软认为扬声器硬件已过时 [7]。当然,过去,DOS 程序员一定知道如何做到这一点,但现在似乎已被遗忘。阅读 ICH 文档 [4] 并进行一些实验得出了以下协议
>写入 端口地址:0x61 值:0x3 |
扬声器已启用。NakedCPU 开始发出连续声音。 |
>写入 端口地址:0x43 值:0xbe >写入 端口地址:0x42 值:0x10 >写入 端口地址:0x42 值:0x1 |
通过写入计时器配置字来更改默认频率。声音停止,因为计时器正在等待接收一个两字节的除数。通过两次事务写入新值 0x0110,之后 NakedCPU 将开始发出高音噪音。 |
>写入 端口地址:0x42 值:0x0 >写入 端口地址:0x42 值:0x20 |
写入更大的除数 0x2000 以降低频率。无需写入配置字。 |
点亮 LED
并行端口越来越过时,但它仍然提供了通过 8 条线读取和发送数据的可能性。奇怪的是,ICH 文档没有提及并行端口的编程。浏览互联网发现,人们对并行端口仍然有一些兴趣,并且可以获得编程信息。将 LED 通过 470 欧姆电阻连接到 D2 端口线,然后按照以下协议操作,该协议演示了并行端口的写入和读取。
>写入 端口地址:0x77A 值:0x34 >写入 端口地址:0x378 值:0x4 |
通过修改扩展控制寄存器 (0x77A),并行端口进入扩展功能端口模式。D2 线变为高电平,LED 发光。 |
>写入 端口地址:0x77A 值:0x34 >写入 端口地址:0x37A 值:0x2c |
也可以从 LPT 读取。通过向端口控制寄存器 (0x37A) 写入 0x2c,我们使并行端口进行读取。LED 微微发光,表明在读取模式下启用了上拉电阻。 |
“第一声哭泣”
处理器在开机后执行的第一条指令是什么?文档[2]说,处理器从地址 0xFFFFFFF0 读取其第一条指令,即 4GB 以下 16 个字节。尝试使用调试器检查此地址是徒劳的(已测试,无效)。为了达到这个高地址,它位于高位 BIOS 的范围内,为 NakedCPU 准备了一个小的可执行文件。该可执行文件定义了一个寻址高位 BIOS 的内存段,并将 4GB 以下 16 个字节的内容发送回主计算机。正如预期,有一个短跳转,大约向下 30KB。可执行文件经过修改,可以下载 4GB 以下到顶部的整个 30KB 内存块。特别好奇的探究者欢迎调查已保存并可供下载的内容。总的印象是,可以看到许多对 PCI 总线的访问和对 CPUID 指令的调用。这当然是有道理的,因为必须设置各种设备,并且 BIOS 正在尝试确定正在使用的处理器。
网络
通过网络通信使用媒体访问控制器(MAC)完成。文档可在英特尔网页上查阅。阅读 IEEE 802.3-2008 标准的前三章也有助于了解底层网络术语以及通过线路发送的数据包格式 [8]。
我们将再次使用 NakedCPU Explorer 调查媒体访问控制器的内部结构并进行一些实验。MAC 需要内存中的数据结构和通过 I/O 地址空间的配置事务。首先,我们必须确定 MAC 的 I/O 地址,这称为基地址 2 (BAR2)。该地址存储在 PCI 配置空间的 bus 0, device 25, function 0 (B0:D25:F0) 寄存器 0x18 中。顺便说一句,文档中引用同一个寄存器存在混淆。ICH 将此特定寄存器称为 MBARC [4],而 MAC 文档 [6] 将其称为 BAR2。通过 NakedCPU Explorer 执行 PCI 事务,如下所示
>pci 输入总线设备功能 0x寄存器:0 25 0 0x18 >read32 端口地址:0xcfc ecc1 |
PCI事务由两步组成:定义位置和读取内容。 |
数字 0xECC1 意味着 I/O 地址实际上是 0xECC0,其第 0 位硬编码为 1,表示该地址确实位于 I/O 空间中,而不是内存映射的 [6]。后者的指示很重要,因为与 MAC 的所有配置和通信也可以使用内存映射寄存器完成,这更快,但对于我们的实验,使用 I/O 空间就足够了,因为它更简单,并且与内存映射操作达到相同的结果。
为了与 MAC 交互,查询者将 MAC 内寄存器的地址写入 BAR2 I/O 地址 (0xECC0)。之后,BAR2+0x4 (0xECC4) 成为该 MAC 寄存器值的窗口。需要注意的是,BAR2 和 BAR2+0x4 只接受 32 位双字读/写操作。MAC 寄存器有大量的位需要处理,有些位相互依赖。仅仅通过查看寄存器的十六进制值很难理解其设置。Excel 工作表 InterpretRegister.xls(可供下载)包含一个宏,可在此情况下提供很大帮助。具体来说,包含位描述的表格应从文档 PDF 中复制粘贴,寄存器的十六进制值将转换为二进制 1 和 0,并显示在描述文本旁边的相应单元格中,参见图 4。
让我们检查控制 CTRL(0x0) 和状态 STATUS(0x8) 寄存器;寄存器地址在括号中给出。拔掉网线后通电
>写入32 端口地址:0xecc0 值:0 >read32 端口地址:0xecc4 100240 |
这种特定的位组合,除其他外,决定了速度和全/半双工的自动配置是否启用。 |
>写入32 端口地址:0xecc0 值:0x8 >read32 端口地址:0xecc4 80080600 |
这些位表示没有建立链接,但初始化已完成。 |
将主计算机与 NakedCPU 连接后,CTRL 寄存器如预期保持不变,而状态寄存器变为 0x80080683。新值表示全双工通信,已建立链接,速度为 1Gbps。运行 Windows XP 的主计算机报告了相同的通信参数,这表明 NakedCPU 网络接口能够在硬件级别与主计算机的接口协商。
接收和解释网络数据包
在本节中,我们将实验读取源自主计算机的网络数据包。我们发现,当主计算机通过网线检测到活动的 NakedCPU 时,Windows 开始生成 DHCP 请求。这些请求是尝试获取 IP 地址和其他高级网络设置,因为 Windows 错误地假设 NakedCPU 是一个路由器或网络服务器。虽然 Windows 搞错了,但这对于我们的实验来说完全没问题,因为我们可以捕获这些数据包并检查它们。
MAC 使用直接内存访问来存储接收到的数据。我们必须创建几个描述符,这些描述符将告诉 MAC 将数据写入何处。因此,需要两个内存范围:一个用于描述符,另一个用于数据包。参考表 1,可以看到地址 0x100000 上方有一块内存区域可用于查询者的数据。考虑到 NakedCPU Explorer 使用一小部分内存来存储传入命令,我们可以安全地使用 0x100500 以上的地址。对于初始实验来说,创建两个描述符就足够了。一个描述符是一个包含四个双字(16 字节)的数据结构。前两个双字是存储数据包的 64 位物理地址。通过 NakedCPU Explorer 写入内存位置的能力,我们可以在地址 0x100500 处创建描述符,指向位于地址 0x101000 和 0x101200 的两个 512 字节长的缓冲区。描述符的集合称为“队列”。MAC 完成存储数据包后,它将更新描述符以指示接收到的数据包大小、错误和其他几个参数。
>内存写入 0x 地址高于 0x100000:0x100500 双字数量:8 0x101000 0 0 0 0x101200 0 0 0 |
命令“memwrite”要求提供地址和要写入的双字数量。请注意,写入 0x100000 以下将导致通用保护错误并重新启动 NakedCPU。 |
MAC 必须知道描述符的位置、接收缓冲区的大小以及要接收的数据包类型。这些信息必须存储在几个 MAC 寄存器中。RDBAL0(0x2800) 和 RDBAH0(0x2804) - 分别是队列基址的 64 位物理地址的低位和高位。RDLEN0(0x2808) - 为队列分配的内存缓冲区的长度。RDH0(0x2810) 和 RDT0(0x2818) - 分别是头指针和尾指针。RFCTL(0x5008) - 接收过滤器控制寄存器。RXCSUM(0x5000) - 接收校验和控制寄存器。在设置这些寄存器之前,CTRL(0) 寄存器的位 26 必须设置为 1,这将导致 MAC 重置。设置所有寄存器后,通过写入 RCTL(0x100) 来设置“启用”位、接收缓冲区接收模式的大小和描述符类型来启动数据接收。
对于 CTRL、RCTL、RFCTL 和 RXCSUM 寄存器,工作表 InterpretRegister.xls 显示了将在我们的实验中使用的值(和位状态)。RDLEN 的值含义有些令人困惑。根据文档,队列缓冲区的长度必须是 128 的倍数,这意味着队列中必须至少有八个描述符(128 / 16)。但是,我们只有两个描述符。我通过实验确定,告诉 MAC 队列缓冲区比实际需要的更大并不是问题,只要 RDT0 寄存器指向实际队列的末尾。因此,对于我们的特定实验,我们必须设置 RDLEN = 0x80,RDT0 = 0x2。
在 NakedCPU 接收数据包之前,主计算机不应发送 DHCP 数据包。为了抑制源自主计算机的 DHCP 数据包,应在管理员帐户下的 Windows 命令行工具中输入以下内容
ipconfig /release
接下来,将表 2 的列按 I - V 的顺序复制并粘贴到 NakedCPU Explorer 中。请注意,在第 4 步结束时,MAC 已准备好启用接收。该步骤以从状态寄存器读取结束,结果应为 0x80683。此值与之前描述的值(0x80080683,第 8 页)相似,不同之处在于位 31 已清除,这表示 DMA 时钟不能降低到其值的 ¼。MAC 改变 DMA 时钟“想法”的原因尚不清楚,但这与我们的实验无关。
我 | 二 | 三 | 四 | V |
写入32 0xecc0 0 写入32 0xecc4 0x4100240 写入32 0xecc0 0x2800 写入32 0xecc4 0x100500 |
写入32 0xecc0 0x2804 写入32 0xecc4 0 写入32 0xecc0 0x2808 写入32 0xecc4 0x80 |
写入32 0xecc0 0x2810 写入32 0xecc4 0 写入32 0xecc0 0x2818 写入32 0xecc4 0x2 |
写入32 0xecc0 0x5008 写入32 0xecc4 0x8000 写入32 0xecc0 0x8 读取32 0xecc4 |
写入32 0xecc0 0x100 写入32 0xecc4 0x402800A |
要启动主计算机发送数据包,请在管理员帐户下的命令行窗口中键入以下内容
ipconfig /renew
从网络读取后,MAC 更新了两个描述符。使用 memread 命令,通过从地址 0x100500 开始读取 8 个双字来观察新值。您会看到描述符的地址字段已更改,并且出现了两个附加值。图 5 显示了我的计算机产生的新值。有关字段的详细信息在 MAC 文档 [6] 中提供。简而言之,数据包长度为 342 字节 (0x156);没有发生错误,描述符显示为“完成”,并且整个数据包能够适应缓冲区 (0x20073)。
描述符 1 | 描述符 2 |
0 |
0 |
f8270fe8 |
f8270fe9 |
20073 |
20073 |
156 |
156 |
MAC 将实际的两个数据包分别存储在地址 0x101000 和 0x101200;它们的内容在 InterpretRegister.xls 的“数据包”工作表中。图 6 显示了存储数据数组的开头和传输顺序。前六个红色标记的字节是目标(广播)地址,后面是源地址和两字节的长度/类型字段。后一个字段以最高有效字节优先传输 [8],使其值为 0x0800。如果长度/类型字段的值小于或等于 0x05DC,则它指示数据包的长度,否则指示其类型 (Ethertype)。standards.ieee.org 网站理论上提供了特定的 Ethertype 值,但实际上不可能找到实际的值列表。幸运的是,维基百科指向了 ieee.org 网站内部的一个确切 URL [9]。根据标准,互联网协议 (IP) 指定的 Ethertype 为 0x0800。显然,下一步是研究互联网协议的格式,该协议在 RFC894 中描述,该协议指向 RFC791 [10]。
IP 报头字段的传输方式与前面描述的长度/类型字段相似,即最高有效位和最高有效字节优先。与直观的表示法不同,直观的表示法中位 0 是最低有效位,而 RFC791 提供了相反的表示法,位 0 是最高有效位。表 3 显示了接收到的数据包的延续及其特定值与 IP 报头前 32 位字段的分配。重要字段是 IHL 和总长度。IHL 指示报头中 32 位字的个数。在我们的例子中是 5,这意味着根据 RFC791,选项和填充字段被省略了。有趣的是,MAC 报告接收到 342 字节,而 IP 报头指示 328 (0x0148) 字节。14 字节的差异完全合理,因为它们构成了长度/类型字段的两个字节加上目标和源硬件地址的 2*6 字节。
IP 报头的其他所有字段都可依此示例轻松映射。具体来说,字段值为:Identification - 0x0fe8(数据包 1),0x0fe9(数据包 2);Flags and Fragment Offset - 0;Time to Live - 0x80;协议 - 0x11;Header Checksum - 0x29be(数据包 1),0x29bd(数据包 2);源 IP 地址 - 0.0.0.0;目标 IP 地址 - 255.255.255.255。可以理解,主 PC 在进行动态主机配置过程时正在请求 IP 地址,因此源 IP 地址全为零。对于目标地址,地址全为 255.255.255.255,这是一个广播地址,类似于硬件广播地址(6 字节,全 255)。
位 | 0 - 3 | 4-7 | 8-15 | 16-31 |
... 00450008 E80F4801 ... |
版本 0x4 |
互联网报头长度(IHL) 0x5 |
服务类型 00 |
总长度 0x0148 |
协议字段的值为 0x11 (17);根据 RFC790,它指的是用户数据报协议 (UDP)——下一个数据封装层——该协议在 RFC768 中描述。根据该文档,UDP 报头包含四个 16 位字。按照传输顺序,这些字是源端口、目的端口、长度和校验和。我的实验中 NakedCPU 接收到的值为源端口 - 0x0044,目的端口 - 0x0043,长度 - 0x0134,校验和 - 0x5c1a(数据包 1)和 0x581a(数据包 2)。308 (0x134) 字节的长度是合理的,因为 IP 报头报告的总数据报长度为 328 字节减去 IP 报头占用的 20 字节。源端口和目的端口号本应在 RFC790 中解释,但结果是,一长串其他文档使该文档过时。在链的末尾,建议查看互联网号码分配机构 (IANA) 的网站,不幸的是,该网站上的信息组织不善。然而,在过时的 RFC 中,可以找到端口号 67 (0x43) 和 68 (0x44) 分别对应于引导协议服务器和客户端端口。引导协议在 RFC1542 中描述,该文档指向 DHCP 协议,参见 RFC2131。本节讨论的所有字段的摘要在图 7 中提供。
结论和未来展望
我们可以在没有任何旨在让我们的生活“更轻松”的未知中间代码层的情况下,直接实验 Intel CPU 和其他 PC 硬件。迄今为止,处理器本身拥有最全面的文档 [2]。其他硬件文档不完善,这就是为什么需要付出大量努力从互联网上收集信息并直接进行实验的原因。处理硬件的旧书以 DOS 为导向,并且严重过时。新硬件被隐藏在未知代码层之后。
理论上,NakedCPU 平台使开发人员能够仅使用所需组件创建特定任务的应用程序。例如,如果我们要谈论一个大型数据库,则无需支持 GUI、USB 即插即用、声卡、.NET 和许多其他东西。NakedCPU 平台的额外好处是免疫病毒。就像在生物学中,灵活的病毒攻击进化良好的生物体一样,计算机病毒攻击发展良好的操作系统。而使用 NakedCPU,特定的任务专用解决方案可能非常独特;因此,病毒制造者将根本没有足够的信息来探索潜在的安全漏洞。
原始出版物
原始出版物可在我的网站上查阅。点击图片
参考文献
- Intel Core 2 Duo 处理器 E8000 和 E7000 系列。数据手册。2009 年 6 月。PDF
- Intel 64 和 IA-32 架构软件开发人员手册,所有卷合集 PDF
- Intel 4 系列芯片组家族。数据手册 PDF
- Intel I/O 控制器集线器 10 (ICH10) 系列。数据手册。PDF
- M Klasson。HexIt - 十六进制编辑器 在此
- Intel I/O 控制器集线器 8/9/10 和 82566/82567/82562V 软件开发人员手册。PDF
- MSDN,Beep 函数。在此
- 信息技术 IEEE 标准 - 系统间电信和信息交换 - 局域网和城域网 - 具体要求。第 3 部分:载波侦听多路访问/冲突检测 (CSMA/CD) 访问方法和物理层规范。IEEE Std 802.3 -2008。在此
- 维基百科指向的以太网类型值列表。在此
- 所有 RFC 文档均可在 http://www.ietf.org/rfc/rfcZZZZ.txt 查阅,其中 ZZZZ 是文档的四位数编号,如有必要,前面补零。也可在此处查阅 所有 RFC
附录
MBR 剖析
描述 | MBR 位置 |
处理器在开机后仍处于实模式时确定当前地址。BIOS 已将 MBR 加载到内存中的某个位置,并将控制权转移给我们的代码。当前地址对于定位伪描述符的物理地址是必要的,伪描述符又定义了全局描述符表 (GDT) 的物理地址和限制。 | 0x3E - 0x4D |
LGDT 指令(加载 GDT 寄存器)正在加载伪描述符,该伪描述符指向 GDT。 | 0x52 |
GDT 和中断描述符表 (IDT) 被复制到新的内存位置,从线性地址 0x0 开始。GDT 和 IDT 定义了处理器在保护模式下操作的内存段。 | 0x57 - 0x64 |
MBR 包含一个非常微小的 32 位保护模式“操作系统”,我们称之为 NakedOS。 | 0x80 - 0x186 |
NakedOS 被复制到从线性地址 0x800 开始的新内存位置。 | 0x65 - 0x71 |
通过使用 LMSW 指令调整机器状态字来切换到保护模式。 | 0x72 - 0x78 |
将控制权转移给 NakedOS。 | 0x7B |
设置 8259 中断控制器 | 0xF5 - 0x105 |
将控制权转移给查询者的可执行文件 | 0x106 |
关键数据结构
结构 | MBR 位置 |
伪描述符 IDT | 0x194 |
伪描述符 GDT | 0x1BA |
空描述符 | 0x1C0 |
MBR 列表:NakedOS
Address Opcode Mnemonic
00000000 EB3C jmp short 00000003E
.............some remnants from FAT...............................
0000003E FA cli
0000003F 33C0 xor ax,ax
00000041 8ED0 mov ss,ax
00000043 BC007C mov sp,07C00
00000046 16 push ss
00000047 07 pop es
00000048 0E push cs
00000049 1F pop ds
0000004A E80000 call 00000004D
0000004D 89E5 mov bp,sp
0000004F 8B5E00 mov bx,[bp+000]
00000052 0F01976D01 lgdt q.[bx+0016D]
00000057 89DE mov si,bx
00000059 81C67301 add si,0173
0000005D B94000 mov cx,040
00000060 31FF xor di,di
00000062 FC cld
00000063 F3 repe
00000064 A4 movsb
00000065 BF0008 mov di,0800
00000068 89DE mov si,bx
0000006A 83C633 add si,033
0000006D B93B01 mov cx,013B
00000070 F3 repe
00000071 A4 movsb
00000072 0F01E0 smsw ax
00000075 0D0100 or ax,01
00000078 0F01F0 lmsw ax
0000007B EA00001000 jmp 00010:00000
-----from here 32-bit code segment ------------------------------
00000080 66B81800 mov ax,018
00000084 8ED0 mov ss,ax
00000086 BCFE030000 mov esp,03FE
0000008B 66B80800 mov ax,08
0000008F 8EC0 mov es,ax
00000091 E492 in al,092
00000093 0C02 or al,02
00000095 E692 out 092,al
00000097 66BAFB03 mov dx,03FB
0000009B B083 mov al,083
0000009D EE out dx,al
0000009E 66BAF803 mov dx,03F8
000000A2 66B80600 mov ax,06
000000A6 66EF out dx,ax
000000A8 66BAFB03 mov dx,03FB
000000AC B003 mov al,03
000000AE EE out dx,al
000000AF 31C0 xor eax,eax
000000B1 BF00020000 mov edi,0200
000000B6 B900020000 mov ecx,0200
000000BB F3 repe
000000BC AA stosb
000000BD BF00030000 mov edi,0300
000000C2 BE14010000 mov esi,0114
000000C7 0E push cs
000000C8 1F pop ds
000000C9 0F011E lidt q.[esi]
000000CC 83C606 add esi,06
000000CF B904000000 mov ecx,04
000000D4 F3 repe
000000D5 A5 movsd
000000D6 66B83800 mov ax,038
000000DA 8EC0 mov es,ax
000000DC 8ED8 mov ds,ax
000000DE 31FF xor edi,edi
000000E0 CD20 int 020
000000E2 66B82800 mov ax,028
000000E6 8EC0 mov es,ax
000000E8 8ED8 mov ds,ax
000000EA 31FF xor edi,edi
000000EC CD20 int 020
000000EE 6631C0 xor ax,ax
000000F1 8EC0 mov es,ax
000000F3 8ED8 mov ds,ax
000000F5 66BA2000 mov dx,020
000000F9 B011 mov al,011 //ICW1
000000FB EE out dx,al
000000FC B028 mov al,028 //ICW2
000000FE 42 inc edx //IRQ0 base addr 0x28
000000FF EE out dx,al
00000100 B004 mov al,04 //ICW3: Slave
00000102 EE out dx,al //connected to pin 2
00000103 B001 mov al,01 //ICW4
00000105 EE out dx,al
00000106 EA000000003000 jmp 00030:000000000
.............random bytes......................................
00000140 66BAFD03 mov dx,03FD
00000144 EC in al,dx
00000145 A801 test al,01
00000147 74FB jz short 000000144
00000149 66BAF803 mov dx,03F8
0000014D EC in al,dx
0000014E AA stosb
0000014F 50 push eax
00000150 66BAFD03 mov dx,03FD
00000154 EC in al,dx
00000155 A820 test al,020
00000157 74FB jz short 000000154
00000159 58 pop eax
0000015A 66BAF803 mov dx,03F8
0000015E EE out dx,al
0000015F C3 ret
00000160 57 push edi
00000161 B904000000 mov ecx,04
00000166 E8D5FFFFFF call 000000140
0000016B E2F9 loop 000000166
0000016D 5F pop edi
0000016E 8B0F mov ecx,[edi]
00000170 85C9 test ecx,ecx
00000172 7409 jz short 00000017D
00000174 51 push ecx
00000175 E8C6FFFFFF call 000000140
0000017A E2F9 loop 000000175
0000017C 59 pop ecx
0000017D CF iretd
0000017E AC lodsb
0000017F E8CBFFFFFF call 00000014F
00000184 E2F8 loop 00000017E
00000186 CF iretd
.............random bytes.........................................
00000194 FF01
00000196 0002
00000198 0000
0000019A E000
0000019C 1000
0000019E 008E0000
000001A2 FE00
000001A4 1000
000001A6 008E0000
000001AA 0000
000001AC 0000
000001AE 0000
000001B0 0000
000001B2 0000
000001B4 0000
000001B6 0000
000001B8 0000
000001BA FF01
000001BC 0000
000001BE 0000
000001C0 0000
000001C2 0000
000001C4 0000
000001C6 0000
000001C8 FF03
000001CA 0000
000001CC 00920000
000001D0 0A13
000001D2 0008
000001D4 009A4000
000001D8 FF03
000001DA 0004
000001DC 00924000
000001E0 FF0F
000001E2 00800B92
000001E6 0000
000001E8 007800
000001EB 0010
000001ED 92
000001EE 8000FF
000001F1 FF3B
000001F3 0900
000001F5 98
000001F6 40
000001F7 00FF
000001F9 FF3B
000001FB 0900
000001FD 92
000001FE 0000
Firstsectwrite
#include "stdafx.h"
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
TCHAR destDisk[] = TEXT("\\\\.\\X:");
cerr << "\n firstsectwrite.exe E: nakedos_x_y.bin";
if (argc != 3)
{
cerr << "\nInput parameters error";
return -1;
}
if(_tcslen(argv[1]) != 2)
{
cerr << "\nInput parameters error";
return -1;
}
_tcscpy(&destDisk[4], argv[1]);
HANDLE hD=CreateFile(destDisk,GENERIC_WRITE, FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING, NULL);
HANDLE hS=CreateFile(argv[2],GENERIC_READ, NULL, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if((INVALID_HANDLE_VALUE == hS) || (INVALID_HANDLE_VALUE == hD))
{
cerr << "error";
return -1;
}
BYTE buf[512];
BYTE bufproof[512];
DWORD dwCopied;
ReadFile(hS, buf, 512, &dwCopied, NULL);
WriteFile(hD, buf, 512, &dwCopied, NULL);
CloseHandle(hD);
CloseHandle(hS);
hD=CreateFile(destDisk,GENERIC_READ, FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING, NULL);
if(INVALID_HANDLE_VALUE == hD)
{
cerr << "error";
return -1;
}
ReadFile(hD, bufproof, 512, &dwCopied, NULL);
if(!equal(buf, buf + 512, bufproof))
{
cerr << "write error";
return -1;
}
return 0;
}