分区体系有DOS、Apple、BSD等,Windows分区使用的是DOS分区体系,用的是MBR分区表,一般我们的硬盘用的都是这种方式。MBR分区表下,LBA(Logical Block Address,逻辑块地址)地址最大为32位,按每扇区512Byte计算最大能描述2TB的空间大小,若硬盘容量超过2TB左右便无法全部描述。除此之外,还有一种分区表叫作GPT(GUID Partition Table,全局唯一标识符分区表),这种分区表用的寻址方式是64位,不受前者的限制。不过,以目前的情况,2TB硬盘虽然并不少见,但是通常我们也不用到,何况现今固态硬盘(SSD)已成为主流,所以Win操作系统中MBR分区表仍然是大部分人的选择,而Linux下用的则是GPT分区表。下面我们来谈一下MBR分区表的结构,关于GPT分区表以后再谈。
首先看硬盘。我们都知道,硬盘上存在着所谓的柱面(Cylinder)、磁头(Head)和扇区(Sector),也就是所谓的CHS,通过CHS地址,我们可以定位到硬盘的唯一一个扇区。此外,还有种地址叫做LBA,也就是硬盘逻辑地址,物理硬盘上每磁道一般有63个扇区,每扇区从1开始编号,而逻辑地址下,扇区从0编号,直到最大扇区号,最大扇区号为硬盘扇区总数减1。所以说,U盘等移动存储设备也是有“扇区”的,这里的扇区对应的是U盘芯片里的数据块(Block)。另外,对物理硬盘,柱面和磁道差不多是一样的概念,它们也就是一个个的同心圆,但从微观角度看扇区是矩形而非扇形。硬盘下每扇区一般是512字节,这是一种约定俗成的标准(最新的SSD可能会有1024字节),磁头是最少的,一般有几个到十几个不等,不过,如果你用Diskgen软件查看硬盘会发现,可能会显示255个磁头,硬盘每盘片上一般有两个磁头,硬盘一般只有几张盘片,怎么可能有255个磁头呢?甚至连U盘也显示255个磁头,这是怎么回事呢?其实这里的磁头是指逻辑磁头数,而非硬盘物理磁头数,Windows操作系统会把硬盘分为每扇区字节数(512),每磁道扇区数(63),每柱面磁道数(255),以及柱面数(上万个),这里少个磁头数,因为以上这些都操作系统定义的逻辑数据,而非硬盘真实的结构,前面说了,对物理硬盘而言柱面和磁道是等价的东西,而Diskgen显示的磁头数应该是磁道数(估计连diskgen自己也不清楚,按照Windows的DISK_GEOMETRY结构定义,成员TracksPerCylinder的意思确实是“每柱面磁道数”,看来Windows把柱面和磁道分开定义了。但请注意,实际硬盘上,柱面就是磁道,磁道就是柱面,两者没有区别),如果用Everest软件查看会发现,硬盘的磁头数仅为十几个,而柱面数仍然为几万个。
下面看分区表。用WinHex打开物理硬盘,第0号扇区就是MBR(Master Boot Recorder,主引导记录),前446字节为引导代码,第447字节也就是偏移0x1BE处开始就是主分区表,大小为64个字节,最后两字节是55AA标志。分区表中共有4个分区表项,每个项大小为16字节,第1个字节为可引导标志,0表示不可引导,0x80表示可引导,之后3个字节是分区起始CHS地址,然后是分区标志,占1字节,例如0x0F或0x05代表扩展分区,0x17代表隐藏NTFS分区,然后是分区结束CHS地址,也是3字节,最后两个是分区起始LBA和分区大小扇区数,各占4字节。这里的4个分区都叫做主分区,如果是扩展分区也可以叫主扩展分区。主分区表中最多可建4个主分区,但扩展分区只能建1个。主分区一般描述C盘,而主扩展分区同样描述了另一个空间,可以把这个空间当作一个新的磁盘,在这个空间上可以建立若干逻辑分区(D、E、F)。主扩展分区的起始处一个扇区为引导记录(可以称作EBR,扩展引导记录),同样的位置0x1BE处又描述了一张分区表,可称之为扩展分区表,这个表只能表示两个分区表项,第1个表项为文件系统分区(一般为第二个逻辑盘),属于二级文件系统分区,第2个表项为二级扩展分区,然后二级扩展分区的EBR中又能表示两个分区,第1个分区为三级文件系统分区,第2个分区为三级扩展分区,所有扩展分区的结束部分一般会到头,也就是会占满硬盘,否则会多出空闲空间。照这个方式类推,最终遍历完所有分区的结构。分区一般为主分区和逻辑分区,扩展分区并不能算真正意义的分区,而是属于硬盘分区后的剩余空间,所以通常我们说到分区,一般不包括扩展分区。每个分区(不包括扩展分区)的开始一个扇区为DBR(DOS Boot Recorder,DOS引导记录),里面记录着分区的信息,比如分区大小总扇区数、每扇区字节数、每簇扇区数、保留扇区数、根目录簇等文件系统信息。
知道了以上这些要素,我们可以动手写一个自己的分区工具了。下面的这个分区程序只是个测试,功能比较简单,大概的功能有快速分区并格式化、调整分区信息、删除分区、隐藏分区、设置活动分区、重建MBR等。由于代码太长,所以只例举了部分最核心的代码。
核心代码:
'//枚举物理磁盘
Public Function m_EnumDisk() As Long
Dim i As Long
Dim n As Long
Dim bytBuffer() As Byte
Dim sdd As STORAGE_DEVICE_DESCRIPTOR
Dim szName As String
Dim szId As String
Dim nDev As Long
Dim DiskId() As Long
nDev = EnumDiskDevice(DiskId) '枚举所有磁盘
If nDev = 0 Then
nDev = 16 '强制遍历16个设备
ReDim DiskId(nDev - 1)
For i = 0 To nDev - 1
DiskId(i) = i
Next i
End If
ReDim bytBuffer(0 To 7)
Erase DiskData
For i = 0 To nDev - 1
If m_OpenDisk(DEVICE_NAME & DiskId(i), True) Then
lResult = DeviceIoControl(hDisk, IOCTL_DISK_GET_LENGTH_INFO, ByVal 0&, 0, bytBuffer(0), 8, dwOutBytes, ByVal 0&)
sdd.Size = LenB(sdd)
GetDiskAttr sdd
'磁盘固件信息,制造商和产品Id
If sdd.VendorIdOffset >= 36 Then
szName = String(512, Chr(0))
CopyMemory ByVal StrPtr(szName), sdd.RawDeviceProperties(sdd.VendorIdOffset - 36), 512 - (sdd.VendorIdOffset - 36)
szName = Trim(TrimNull(StrConv(szName, vbUnicode)))
End If
If sdd.ProductIdOffset >= 36 Then
szId = String(512, Chr(0))
CopyMemory ByVal StrPtr(szId), sdd.RawDeviceProperties(sdd.ProductIdOffset - 36), 512 - (sdd.ProductIdOffset - 36)
szId = Trim(TrimNull(StrConv(szId, vbUnicode)))
End If
ReDim Preserve DiskData(n)
With DiskData(n)
.DiskId = DiskId(i)
CopyMemory .ddSize, bytBuffer(0), 8
.ddSize = .ddSize * 10000 '总容量,字节
.ddBusType = sdd.BusType '总线
.ddName = szName & " " & szId
.ddBPS = BytesPerSector 'BPS,每扇区字节数
End With
n = n + 1
m_CloseDisk
End If
Next i
Erase bytBuffer
m_EnumDisk = n
End Function
'//获取分区信息
Public Function m_GetPartition(ByVal DiskId As Long, Optional ByVal bGetDriveName As Boolean) As Long
If m_OpenDisk(DEVICE_NAME & DiskId, True) = False Then
MsgBox "无法打开磁盘。" & DEVICE_NAME & DiskId & " 请以管理员运行,并退出其它程序。", vbCritical
Exit Function
End If
Erase PartData
Dim bytBuf() As Byte
Dim n As Long
Dim i As Long
Dim bIsExt As Boolean
ReDim bytBuf(BytesPerSector - 1)
lResult = m_ReadDisk(0, bytBuf())
Dim ptd(3) As PART_TABLE_DATA
CopyMemory ptd(0), bytBuf(PT_OFFSET), PT_SIZE * 4
For i = 0 To 3
If ptd(i).StartLBA <> 0 Then
bIsExt = IsExtPart(ptd(i).PartFlag)
ReDim Preserve PartData(n)
With PartData(n)
.PartTable = ptd(i)
.PTSector = 0
.PTIndex = i
.StartLBA = ptd(i).StartLBA
.SecCount = ptd(i).cntSectors
End With
If Not bIsExt Then
PartData(n).FileSystem = GetFileSys(ptd(i).StartLBA) '文件系统
PartData(n).PartType = ptMain
Else
PartData(n).FileSystem = FILE_SYSTEM.Extended
PartData(n).PartType = ptExtended
End If
n = n + 1
If bIsExt Then '扩展分区
ExtPartLBA = ptd(i).StartLBA '扩展分区LBA
GetSubPart ptd(i).StartLBA, n
End If
End If
Next i
'未使用部分
Dim DiskSec As Currency
Dim StartSec As Currency
Dim UnusedSec As Currency
Dim EndSec As Currency
Dim bFlag As Boolean
DiskSec = vSectorCount '物理扇区数
If n = 0 Then
bFlag = True
StartSec = 0
UnusedSec = DiskSec
Else
EndSec = ToInt64((PartData(n - 1).StartLBA)) + PartData(n - 1).SecCount
If EndSec < DiskSec Then
bFlag = True
StartSec = EndSec
UnusedSec = DiskSec - EndSec
End If
End If
If bFlag Then
ReDim Preserve PartData(n)
With PartData(n)
.StartLBA = StartSec
.SecCount = UnusedSec
.PartType = ptUnused
End With
n = n + 1
End If
If bGetDriveName Then
'API获取分区参数,以获取分区字母 //仅供参考,物理方式获取分区不一定与逻辑驱动器对应
'//若逻辑分区存在错误,比如根目录簇非.法,则可能会死锁 WinHex打开磁盘时也会死掉(遍历读取逻辑分区引起),只能用Diskgen修改物理磁盘数据
Dim pdi() As PARTITION_DEVICE_INFO
Dim cntPI As Long
Dim j As Long
Dim Id As Long
Id = 0
cntPI = EnumPartitionInfo(pdi)
For i = 0 To cntPI - 1
If DiskId = pdi(i).DeviceId Then
Id = 0
For j = 0 To n - 1
If PartData(j).PartType = ptMain Or PartData(j).PartType = ptLogic Then
Id = Id + 1
If Id = pdi(i).PartitionId Then
PartData(j).DriveName = pdi(i).DriveName '逻辑驱动器字母,隐藏分区同样占编号
Exit For
End If
End If
Next j
End If
Next i
End If
Erase bytBuf
m_GetPartition = n
m_CloseDisk
End Function
'//获取子分区
Private Function GetSubPart(ByVal PLBA As Long, Idx As Long) As Long
Dim i As Long
Dim bytBuf() As Byte
Dim ptd(3) As PART_TABLE_DATA
Dim n As Long
Dim cnt As Long
Dim nFlag As Long
ReDim bytBuf(BytesPerSector - 1)
If m_ReadDisk(PLBA, bytBuf()) = 0 Then Exit Function
CopyMemory ptd(0), bytBuf(PT_OFFSET), PT_SIZE * 4
'Debug.Print bytBuf(0), bytBuf(1)
n = 0
For i = 0 To 1
If ptd(i).StartLBA <> 0 Then
nFlag = 0
If IsExtPart(ptd(i).PartFlag) Then
cnt = GetSubPart(ExtPartLBA + ptd(i).StartLBA, Idx) '递归
If cnt = 0 Then nFlag = 1 '如果没有下级分区,保留扩展分区
Else
nFlag = 2 '是逻辑分区
End If
If nFlag <> 0 Then
ReDim Preserve PartData(Idx)
With PartData(Idx)
.PartTable = ptd(i) '分区表
.PTSector = PLBA '分区表所在扇区
.PTIndex = i '分区表索引
.StartLBA = PLBA + ptd(i).StartLBA 'LBA绝对地址
.SecCount = ptd(i).cntSectors
If nFlag = 2 Then
.PartType = ptLogic
.FileSystem = GetFileSys(.StartLBA)
Else
.PartType = ptExtended
.FileSystem = FILE_SYSTEM.Extended
End If
End With
Idx = Idx + 1
End If
n = n + 1
End If
Next i
GetSubPart = n
Erase bytBuf
End Function