在C#开发中,把任意文件转换成十六进制字符串并输出标准的Hex Dump,是分析二进制格式、排查数据损坏、对接底层协议时的常见需求。Hex Dump并不是简单把文件当文本读出来,而是要以字节为单位,将每个字节的值用两位十六进制表示,同时记录行偏移并尽量还原可打印字符。

一、文件字节读取的底层原理
计算机中的文件在存储层面都是连续的字节流。C#提供了多种读取方式,其中最基础且可控的是FileStream类。它直接对接操作系统的文件句柄,支持以指定缓冲区大小分块读取,避免一次性把大文件装入内存。与之相对,File.ReadAllBytes虽然写法简单,但会在内部申请与文件等大的字节数组,遇到几百兆的文件就容易引发内存压力。
在获取Hex Dump时,我们关心的是原始字节而非字符编码。如果错误地使用File.ReadAllText配合Encoding去读,框架会按编码规则解释字节,像UTF-8遇到非法续字节可能变成替换符,导致Dump失真。因此正确做法是始终使用字节流API,拿到byte[]后再做格式化。
1.1 为什么需要偏移量
标准Hex Dump工具(如Linux的xxd)每行显示十六进制偏移,例如00000000、00000010。偏移量让我们能快速定位某个字节在文件中的绝对位置。在C#中实现时,只需维护一个long position变量,每读一行就加上该行字节数即可。
偏移量通常用8位十六进制展示,足以覆盖4GB以内的文件;更大的文件可以扩展到16位。格式化时用ToString("X8")就能补齐前导零,保证对齐美观。
二、基础实现:小文件整体转换
对于几兆以内的文件,可以放心使用File.ReadAllBytes一次性读取,然后用循环生成十六进制字符串。下面的示例演示了如何输出包含偏移、十六进制区和ASCII区的三栏式Dump。
using System;
using System.IO;
using System.Text;
class HexDumpDemo
{
static void Main()
{
string path = @"D:test.bin";
byte[] data = File.ReadAllBytes(path);
StringBuilder sb = new StringBuilder();
int bytesPerLine = 16;
for (int i = 0; i < data.Length; i += bytesPerLine)
{
// 偏移量
sb.Append(i.ToString("X8"));
sb.Append(" ");
// 十六进制区
for (int j = 0; j < bytesPerLine; j++)
{
if (i + j < data.Length)
{
sb.Append(data[i + j].ToString("X2"));
sb.Append(" ");
}
else
{
sb.Append(" ");
}
}
sb.Append(" ");
// ASCII区
for (int j = 0; j < bytesPerLine; j++)
{
if (i + j < data.Length)
{
byte b = data[i + j];
sb.Append(b >= 32 && b < 127 ? (char)b : '.');
}
}
sb.AppendLine();
}
Console.WriteLine(sb.ToString());
}
}
上述代码把文件每16字节作为一行。十六进制区对每个字节调用ToString("X2")得到两位大写十六进制;ASCII区则判断字节是否落在可打印范围,否则用点号代替。这种方式逻辑直观,适合教学或处理配置类等小文件。
不过要注意,File.ReadAllBytes在文件超过可用内存时会抛出OutOfMemoryException。如果生产环境不确定文件大小,应当改用分块读取方案。
三、大文件方案:分块读取与流式输出
当文件可能达到数百兆甚至上吉字节时,必须用FileStream配合固定缓冲区逐步读取。这样内存中只保留一小块字节,处理完即丢弃。下面示例以8KB为缓冲,边读边写控制台或文件。
using System;
using System.IO;
using System.Text;
class StreamHexDump
{
static void Main()
{
string path = @"D:bigfile.bin";
int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
long position = 0;
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
int read;
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
{
DumpBlock(buffer, read, position);
position += read;
}
}
}
static void DumpBlock(byte[] data, int count, long baseOffset)
{
int bytesPerLine = 16;
for (int i = 0; i < count; i += bytesPerLine)
{
StringBuilder line = new StringBuilder();
line.Append((baseOffset + i).ToString("X8"));
line.Append(" ");
for (int j = 0; j < bytesPerLine; j++)
{
if (i + j < count)
{
line.Append(data[i + j].ToString("X2"));
line.Append(" ");
}
else
{
line.Append(" ");
}
}
line.Append(" ");
for (int j = 0; j < bytesPerLine; j++)
{
if (i + j < count)
{
byte b = data[i + j];
line.Append(b >= 32 && b < 127 ? (char)b : '.');
}
}
Console.WriteLine(line.ToString());
}
}
}
该方案每次从流中读入最多8KB数据,调用DumpBlock处理这一段。由于baseOffset记录了全局偏移,输出的偏移量依旧连续正确。即使文件有数GB,程序常驻内存也仅为缓冲区大小。
如果希望结果写入文本文件而非控制台,只需把Console.WriteLine换成StreamWriter的写入即可。此外,还可以利用BufferedStream包裹FileStream来减少磁盘IO次数,提升吞吐。
四、性能与格式优化建议
在频繁生成Hex Dump的场景下,字符串拼接性能值得关注。示例里使用StringBuilder已经比直接加号拼接高效,但若追求极致,可以预分配字符数组并填充。另外,BitConverter.ToString(byte[])能一行输出十六进制并以短横线连接,虽然格式不同,但转换速度很快,适合只需要十六进制串而不需要ASCII区的情况。
byte[] sample = { 0x48, 0x65, 0x6C, 0x6C, 0x6F };
string hex = BitConverter.ToString(sample).Replace("-", " ");
Console.WriteLine(hex); // 输出 48 65 6C 6C 6F
上例展示了用BitConverter快速得到空格分隔的十六进制串。它的底层用查表法转换,比手动ToString稍快。但如果要完整Hex Dump版式,仍推荐自行控制布局。
最后提醒,某些文件含有BOM或压缩数据,十六进制视图里看到的前几个字节可能就是格式标记。理解这一点,才能在排查文件解析异常时,不被错误的字符推断误导。
五、常见误区总结
新手常把File.ReadAllText读到的字符串再转十六进制,这等于经历了一次编码往返,不可见字节全丢。还有人用string.Join配合Select写得很短,却忘了大文件会撑爆内存。正确思路始终是:字节进、字节出、格式化分离。
只要把握住FileStream分块、byte级格式化、偏移累计这三个要点,无论是小工具还是服务端批量处理,都能写出稳定高效的C# Hex Dump模块。