Python 直接读取硬盘二进制数据

简单来说硬盘整体可以看作是一个文件,并且可以按照常规文件使用seek()、read()、write()等IO函数。当然也有一些人建议操纵这么底层的东西当然是C/C++比较好啦,但是怎么说呢,哪怕是汇编,也要用操作系统给的名字才能访问,而且就快速开发来说,可以比较一下代码的体量,下附Windows下C++的版本。

对于Windows来说

读取硬盘二进制数据

>>> disk=open('\\\\.\\PHYSICALDRIVE1','rb')
>>> disk.seek(0)
0
>>> disk.read(512).hex()
……巨长的一串,能观察到标志性的55AA。

其中\\\\.\\PHYSICALDRIVE1或者说是\\.\PHYSICALDRIVE1是由微软定义(KB100027)的一个进行原始IO操作的设备名,其通式为“\\.\PhysicalDriveN”,N为自然数。
同在KB100027中规定逻辑驱动器的名称为“\\.\X”:,逻辑驱动器也能同上方式打开。
需要注意的是尽管以只读方式打开仍需要管理员权限,然而拥有管理员权限也能写入,故实际操作时需要谨慎。
需要特别注意以Lengcy模式启动的计算机在引导硬盘的前512字节是用作主引导扇区(MBR),如果它受到破坏,硬盘上的基本数据结构信息将会丢失,需要用繁琐的方式试探性的重建数据结构信息后才可能重新访问原先的数据。基本数据结构信息丢失的一个显而易见的后果就是该硬盘无法再由Lengcy模式引导系统。
对于Linux来说
同样的方法,只不过设备名改成df命令所显示的FileSystem项,例如/dev/xvda1等,也是可以这样简单打开的。不过为什么不试试神奇的dd呢

应用

比较浅显一个应用就是在Windows下模拟Linux的dd命令,显然目前很难找到一个看起来很清爽的磁盘备份软件。那我们就可以利用这个,非常方便地做全盘备份。

首先在cmd下用diskpart命令确定磁盘序号

DISKPART> list disk

磁盘 ### 状态 大小 可用 Dyn Gpt
-------- ------------- ------- ------- --- ---
磁盘 0 联机 931 GB 0 B *
磁盘 1 联机 1863 GB 1024 KB * *
磁盘 2 联机 1863 GB 1024 KB * *
磁盘 3 联机 223 GB 0 B *
磁盘 4 联机 7396 MB 0 B
磁盘 5 联机 59 GB 0 B

例如我们要备份磁盘5到E盘tmp目录下的backup文件中,接下来就是用于实现该功能的Python简易源码(感谢 heitaoiq <248193263@qq.com>的关于read方法产生异常的提醒)

import lzma
from time import time

DRIVE = '\\\\.\\PhysicalDrive5'
OUTFILE = 'e:/tmp/backup'
MB = 1024 * 1024
BLOCKSIZE = 1 * MB
T0 = time()


def stat(r, w):
    return 'Read {:,}; Written {:,}; Compress Ratio {:0.2f}%; Speed {:0.2f} MB/s'.format(r, w, 100 * w / r, r / (MB * (time() - T0)))


if __name__ == '__main__':
    with open(DRIVE, 'rb') as disk:
        with open(OUTFILE, 'wb+') as out:
            lzc = lzma.LZMACompressor()
            read = 0
            write = 0
            while True:
                try:
                    source = disk.read(BLOCKSIZE)
                except PermissionError:
                    break
                else:
                    read += len(source)
                    write += out.write(lzc.compress(source))
                    print(stat(read, write), end='\r')
            write += out.write(lzc.flush())
            print(stat(read, write), 'Done.')

****C++的例子****
Author:zuishikonghuan

#include "stdafx.h"  
#include  
  
//参数:输出的字符串指针,开始位置,长度  
//返回值:读取的大小  
DWORD ReadDisk(unsigned char* &out,DWORD start,DWORD size)  
{  
    OVERLAPPED over = { 0 };  
    over.Offset = start;  
    HANDLE handle = CreateFile(TEXT("\\\\.\\PHYSICALDRIVE0"), GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);  
    if (handle == INVALID_HANDLE_VALUE)return 0;  
    unsigned char* buffer = new unsigned char[size + 1];  
    DWORD readsize;  
    if (ReadFile(handle, buffer, size, &readsize, &over) == 0)  
    {  
        CloseHandle(handle);  
        return 0;  
    }  
    buffer[size] = 0;  
    out = buffer;  
    //delete [] buffer;  
    //注意这里需要自己释放内存  
    CloseHandle(handle);  
    return size;  
}  
int _tmain(int argc, _TCHAR* argv[])  
{  
    unsigned char* a;  
    DWORD len=ReadDisk(a, 0, 512);  
    if (len){  
        for (int i = 0; i < len; i++){  
            printf("%02X ", a[i]);  
        }  
    }  
        getchar();  
    return 0;  
}  

加入对话

12条评论

  1. >>> disk=open(‘\\\\.\\PHYSICALDRIVE1′,’rb’)
    >>> disk.seek(0)

    我如果输入disk.seek(0, 2),移动到末尾,会提示error。有什么办法解决吗

    1. 以我目前掌握的情况来看,应该没什么特别显而易见的解决办法,可能属于Python的bug(也许是feature也说不定)。

      首先,Ubuntu上seek(0, 2)是没有任何问题的,所以我想这应该是一个平台相关的问题;
      其次,我个人用的是Python3.6.6,执行 os.stat(disk.fileno()) 会出现[WinError 1]错误。理论上os.stat应该返回这个文件描述符的相关信息,那我想你遇到的error可能与这个错误有关;
      最后,我估计你会考虑组合使用disk.read(…)和disk.tell()来曲线救国(无非速度慢一点),但seek、read、tell这三个函数在末尾附近的行为特别奇怪。比如你可能会考虑用:
      while True:
          print(disk.read(1024),disk.tell())

      最后撞上一个[Errno 13] Permission denied 错误,找到最后一次成功的disk.tell(),就可以了。
      但是你再用disk.seek(…, 0)去寻址到倒数几个位的时候,再用disk.read(1)结果却会出现[Errno 22] OSError: Invalid argument,所以那最后一次disk.tell()给出的offset是否合法也是个问题。

  2. 我通过wmi
    import wmi
    c=wmi.WMI()
    usize= c.Win32_DiskDrive()[0].Size
    size=int(usize)
    获得了c盘的大小,但是我不知道这个数字与python read读到的大小是否一致

    1. 有意思的是并不一致呢。

      我简单测试了一下我的U盘,FAT32分区。
      WMI模块得到7748213760;
      read方法(循环直到[Errno 13])得到7756087296;
      文件资源管理器右键→属性→常规→容量(姑且称之为GUI途径?)得到7738261504;
      并且read方法在7748213760后读出的数据都为\x00
      那么有 GUI < WMI < read 另外一个NTFS分区的U盘: WMI模块得到7789340160;
      read方法得到7790493696;
      GUI途径得到7789441024;
      有意思的是read最后读出的数据似乎是与NTFS文件系统有关的数据。
      那么有 WMI < GUI < read 不知道是否对你有帮助

        1. 可能以前读到末尾没有数据的话会返回”,所以下面判断了一下source的存在性。
          [Errno 13]是permission denied,如果没在open(DRIVE, ‘rb’)处抛出permission denied,我认为可以当作读到了末尾(相当于EOF),你可以选择在read处用try: … except PermissionError: …来终止读取循环

          1. 不会出现有些块读不到吗,我用这种方法把c盘备份了,但是当我用这块备份的盘启动时,windows是损坏的

          2. 这个,我只能说这个脚本达到的效果与dd for windows在读取到的字节数这个意义上,是一致的。见下图
            dd for windows的结果与脚本一致

            那么关于你所提到的备份C盘,姑且认为你是将硬盘移到其他主机,或者是使用其他可移动存储介质启动(例如PE),并且C盘上没有产生任何其他的写操作,例如操作系统运行时产生的各类临时文件(因为任何写操作都会写入NTFS日志,最终导致备份下来的数据不一致,通常情况下会损坏NTFS)。我之前用Ubuntu Live CD中的dd命令成功地备份并还原过Windows系统盘,考虑到dd for windows和我脚本读取到的字节数一致,dd for windows又与Linux系统的dd命令应当一致,我很难理解你这个Windows为什么是损坏的。

            此外你提到有些块读不到,理论上是可能有这种情况的:比如磁盘某些扇区存在物理损坏。对于这类问题dd命令的处理方式是padding zero,而我这个脚本并没有处理这个异常(因为没有测试条件)。

            总的来说我并不清楚你备份的Windows为什么是损坏的,爱莫能助,深表遗憾。

  3. 非常感谢您
    再请问一下
    能不能存在这种情况 ,我在运行中的windows 上运行python程序去读他的块,并备份,windows 一直在运行(可能会产生一些日志),这样是不是导致windows磁盘损坏的原因呢??

    1. 那样的话,是必然会造成损坏的。在线备份操作系统,Windows只能用卷影副本功能(也就是windows自带的备份功能),Linux只能用LVM snapshot(也就是所谓磁盘快照)。离线备份操作系统的话,当然怎么方便怎么搞都行。

        1. 一方面,我个人是非常怀疑你这个Linux的在线备份方法有没有真正实现数据一致性,因为我用duplicity做备份的时候还遇到过还原出来的MySQL数据库异常。

          另一方面,你大可以试试将装有windows的硬盘,挂载到Linux上,然后再尝试备份并还原到另一块硬盘上。如果可行,那说明Windows对系统盘的读写比你想象的多得多;如果还是不行,嗯,只能说 food for thought ¯\_(ツ)_/¯

留下评论