再谈 VC6 编译错误 "Error RC2176: old DIB in res\app.ico" 的原因与解决办法
在使用 Visual C++ 6.0 (VC6) 编译旧项目或维护遗留代码时,开发者经常会遇到如下资源编译错误:
Error RC2176: old DIB in res\app.ico; pass it through SDKPAINT
这个错误提示往往让人摸不着头脑,只能确定是图标文件格式的问题。但到底是什么问题?图标太大了?还是格式不兼容?我最早开始学习编程的时候,用的就是 VC6,当时就碰到过这个错误,但是当时具体怎么解决的已经记不清了,至于根本原因也没有深入了解过。最近在维护一个老项目时再次遇到这个问题,今时不同往日,我也不再是当年的小菜鸟了。经过一番研究和实验,终于弄清楚了其中的根本缘由,接下来我就详细解析该错误产生的原因以及具体的解决办法。
错误原因分析
通过简单的分析和查阅资料,我们可以初步了解到以下几点:
1、这个错误与 ICO 图标文件的内部格式有关。
2、这个错误是VC6的资源编译器RC.exe在处理 ICO 文件时产生的。
接下来,直接祭出工具三件套: Process Monitor、IDA Pro 和 OllyDbg,对 VC6 的资源编译器 rc.exe 进行动态分析和静态反汇编,深入挖掘其处理 ICO 文件的逻辑。
故障重现
用VC6新建一个简单的MFC应用程序,然后将一个有问题的图标的 ICO 文件(随便找一个png文件,用网上的在线转换工具转换成 ICO)添加到资源中进行编译,结果就会出现上述错误提示。
Compiling resources...
D:\Program\testMFC\testMFC.rc (69): error RC2176 : old DIB in res\testMFC.ico; pass it through SDKPAINT
执行 rc.exe 时出错.
构造调试环境
rc.exe是由IDE调用的,为了方便调试,我们需要直接运行 rc.exe,具体的参数格式,我们可以用Process Monitor捕获IDE调用rc.exe时的命令行参数:
rc.exe /l 0x804 /fo"Debug/testMFC.res" /d "_DEBUG" /d "_AFXDLL" "D:\Program\testMFC\testMFC.rc"
由于调试器不能指定启动时的工作目录,所以我直接把rc.exe和需要的rcdll.dll直接拷贝到测试项目目录,运行上述命令行参数启动 rc.exe。
同时,Process Monitor 还可以帮助我们捕获 rc.exe 运行时的文件访问行为,方便我们定位问题。


根据ReadFile操作的调用栈,直接定位到 rc.exe 读取 ICO 文件的代码位置,实际上是在调用 rcdll.dll 中的某个函数处理 ICO 文件。

静态分析 rcdll.dll
接下来我们把 rcdll.dll 加载到 IDA Pro 中进行静态分析(其实我一开始分析的是rc.exe,但是加载的时候发现这个文件特别小,基本是个空壳,主要的逻辑都在 rcdll.dll 中)。
此时分析的rcdll.dll 版本: 5.0.1473.1
经过初步分析之后,我们定位到这样一个函数,它的反编译结果如下:
int __stdcall sub_71C2DAF6(int a1, int a2, int a3)
{
int v3; // edi
_WORD *v4; // esi
bool v5; // zf
int v7; // [esp+Ch] [ebp-60h] BYREF
__int16 v8; // [esp+10h] [ebp-5Ch]
__int16 v9; // [esp+14h] [ebp-58h]
__int16 v10; // [esp+18h] [ebp-54h]
__int16 v11; // [esp+1Ah] [ebp-52h]
_WORD v12[4]; // [esp+34h] [ebp-38h] BYREF
int v13; // [esp+3Ch] [ebp-30h]
int Offset; // [esp+40h] [ebp-2Ch]
__int16 v15; // [esp+44h] [ebp-28h] BYREF
__int16 v16; // [esp+46h] [ebp-26h]
__int16 v17; // [esp+48h] [ebp-24h]
__int16 v18; // [esp+4Ah] [ebp-22h]
int v19; // [esp+4Ch] [ebp-20h]
__int16 Buffer; // [esp+50h] [ebp-1Ch] BYREF
__int16 v21; // [esp+52h] [ebp-1Ah]
unsigned __int16 v22; // [esp+54h] [ebp-18h]
int v23; // [esp+58h] [ebp-14h]
int v24; // [esp+5Ch] [ebp-10h]
_WORD v25[2]; // [esp+60h] [ebp-Ch] BYREF
int v26; // [esp+64h] [ebp-8h]
unsigned int i; // [esp+68h] [ebp-4h]
RcReadFile(g_fp, &Buffer, 6u);
if ( Buffer || v21 != 1 && v21 != 2 )
RclCloseFile(2175u, (int)&dword_71C42F48);
sub_71C2CCA0(&Buffer, 6);
for ( i = 0; i < v22; ++i )
{
RcReadFile(g_fp, v12, 0x10u);
v23 = RcSeekFile(g_fp, 0, 1);
RcSeekFile(g_fp, Offset, 0);
RcReadFile(g_fp, &v7, 0x28u);
if ( v7 != 40 )
RclCloseFile(2176u, (int)&dword_71C42F48);
v18 = v11;
v17 = v10;
RcSeekFile(g_fp, Offset, 0);
v3 = 0;
if ( v21 == 1 )
{
v15 = v12[0];
v16 = v12[1];
}
else if ( v21 == 2 )
{
v25[0] = v12[2];
v25[1] = v12[3];
v15 = v8;
v3 = 4;
v16 = v9;
}
v19 = v3 + v13;
v4 = (_WORD *)sub_71C309B2(40);
v4[18] = word_71C34DB8;
*(_DWORD *)v4 = dword_71C47FA4;
*((_DWORD *)v4 + 1) = dword_71C47FA8;
*((_DWORD *)v4 + 7) = 0;
v4[20] = word_71C35248++;
v4[19] = *(_WORD *)(a2 + 38);
*((_DWORD *)v4 + 4) = v3 + v13;
v24 = sub_71C2AEE9(0, a3);
sub_71C2CCA0(&v15, 12);
if ( dword_71C42C34 )
v26 = sub_71C2DE62(v4[20]);
else
LOWORD(v26) = v4[20];
*(_WORD *)sub_71C2C971(2) = v26;
sub_71C30AB0(dword_71C45994);
sub_71C2AE97(v24, v4, v25, v3, v13);
RcSeekFile(g_fp, v23, 0);
}
v5 = (*(_BYTE *)(a2 + 38) & 0x40) == 0;
*(_DWORD *)(a2 + 16) = 14 * v22 + 6;
if ( v5 )
*(_WORD *)(a2 + 38) = 4144;
fclose(g_fp);
g_fp = 0;
return sub_71C2DAC4(a1, a2);
}
其中的RcReadFile、RcSeekFile是我自己起的名字,是对文件进行读写操作的封装函数。 这里可以看到有文件读取操作,并且出现了我们熟悉的错误码2176。
既然是对ICO文件进行处理,那么我们就需要了解 ICO 文件的格式结构。
ICO 文件格式由两部分组成:文件头(ICONDIR)和图标目录(ICONDIRENTRY),以及实际的图像数据。每个图像数据块通常是以 DIB(Device Independent Bitmap)格式存储的。相关数据结构如下:
#pragma pack(push,1) /* 1 字节对齐,保证大小正确 */
/* -------------- 目录头 -------------- */
typedef struct {
/*off=0x00*/ WORD idReserved; /* 总是 0 */
/*off=0x02*/ WORD idType; /* 1=图标, 2=光标 */
/*off=0x04*/ WORD idCount; /* 图像数量 */
} ICONDIR; /* 共 6 字节 */
/* -------------- 目录项 -------------- */
typedef struct {
/*off=0x00*/ BYTE bWidth; /* 宽度,像素;256 时填 0 */
/*off=0x01*/ BYTE bHeight; /* 高度,像素;256 时填 0 */
/*off=0x02*/ BYTE bColorCount; /* 调色板项数;真彩填 0 */
/*off=0x03*/ BYTE bReserved; /* 保留,必须是 0 */
/*off=0x04*/ WORD wPlanes; /* 颜色平面;图标填 1 */
/*off=0x06*/ WORD wBitCount; /* 位深度(bpp) */
/*off=0x08*/ DWORD dwBytesInRes; /* 图像数据长度(字节) */
/*off=0x0C*/ DWORD dwImageOffset;/* 数据在文件中的偏移 */
} ICONDIRENTRY; /* 共 16 字节 */
/* -------------- BMP 信息头(DIB) -------------- */
typedef struct {
/*off=0x00*/ DWORD biSize; /* 本结构大小,固定 40 */
/*off=0x04*/ LONG biWidth; /* 图宽(像素) */
/*off=0x08*/ LONG biHeight; /* 图高*2(含 AND 掩码) */
/*off=0x0C*/ WORD biPlanes; /* 平面数,1 */
/*off=0x0E*/ WORD biBitCount; /* 位深度(24/32 等) */
/*off=0x10*/ DWORD biCompression; /* 压缩类型;0=BI_RGB */
/*off=0x14*/ DWORD biSizeImage; /* 图像数据大小;0 可省 */
/*off=0x18*/ LONG biXPelsPerMeter; /* 水平分辨率,常填 0 */
/*off=0x1C*/ LONG biYPelsPerMeter; /* 垂直分辨率,常填 0 */
/*off=0x20*/ DWORD biClrUsed; /* 调色板颜色数;0=2^n */
/*off=0x24*/ DWORD biClrImportant; /* 重要颜色数;0=全部 */
} BITMAPINFOHEADER; /* 共 40 字节 */
#pragma pack(pop)
注意一下,这三个结构体的大小,分别是 6 字节、16 字节和 40 字节, 与代码中的数字对应上了。 我们在IDA中用解析C头文件的方式导入以上定义,然后修改相关局部变量的类型,就能更清晰地看到代码逻辑。
修改后的反编译结果如下(函数名字被我改为了ProcessICOFile):
int __stdcall ProcessICOFile(int a1, int a2, int a3)
{
int v3; // edi
char *buffer; // esi
bool v5; // zf
BITMAPINFOHEADER bitmpHead; // [esp+Ch] [ebp-60h] BYREF
ICONDIRENTRY DirEntry; // [esp+34h] [ebp-38h] BYREF
__int16 biWidth; // [esp+44h] [ebp-28h] BYREF
__int16 biHeight; // [esp+46h] [ebp-26h]
WORD biPlanes; // [esp+48h] [ebp-24h]
WORD biBitCount; // [esp+4Ah] [ebp-22h]
DWORD v13; // [esp+4Ch] [ebp-20h]
ICONDIR dir; // [esp+50h] [ebp-1Ch] BYREF
int Offset; // [esp+58h] [ebp-14h]
char *v16; // [esp+5Ch] [ebp-10h]
WORD wPlanes; // [esp+60h] [ebp-Ch] BYREF
WORD wBitCount; // [esp+62h] [ebp-Ah]
int v19; // [esp+64h] [ebp-8h]
unsigned int i; // [esp+68h] [ebp-4h]
RcReadFile(g_fp, &dir, 6u);
if ( dir.idReserved || dir.idType != 1 && dir.idType != 2 )
RclCloseFile(2175u, (int)&dword_71C42F48);
sub_71C2CCA0((char *)&dir, 6);
for ( i = 0; i < dir.idCount; ++i )
{
RcReadFile(g_fp, &DirEntry, 0x10u);
Offset = RcSeekFile(g_fp, 0, SEEK_CUR);
RcSeekFile(g_fp, DirEntry.dwImageOffset, 0);
RcReadFile(g_fp, &bitmpHead, 0x28u);
if ( bitmpHead.biSize != 40 ) // 如果head大小不对
RclCloseFile(2176u, (int)&dword_71C42F48);
biBitCount = bitmpHead.biBitCount;
biPlanes = bitmpHead.biPlanes;
RcSeekFile(g_fp, DirEntry.dwImageOffset, 0);
v3 = 0;
if ( dir.idType == 1 )
{
biWidth = *(_WORD *)&DirEntry.bWidth;
biHeight = *(_WORD *)&DirEntry.bColorCount;
}
else if ( dir.idType == 2 )
{
wPlanes = DirEntry.wPlanes;
wBitCount = DirEntry.wBitCount;
biWidth = bitmpHead.biWidth;
v3 = 4;
biHeight = bitmpHead.biHeight;
}
v13 = v3 + DirEntry.dwBytesInRes;
buffer = AllocBuffer(1064);
*((_WORD *)buffer + 18) = g_wLanguage;
*(_DWORD *)buffer = dword_71C47FA4;
*((_DWORD *)buffer + 1) = dword_71C47FA8;
*((_DWORD *)buffer + 7) = 0;
*((_WORD *)buffer + 20) = word_71C35248++;
*((_WORD *)buffer + 19) = *(_WORD *)(a2 + 38);
*((_DWORD *)buffer + 4) = v3 + DirEntry.dwBytesInRes;
//... 省略后续代码;
}
可以看到,关键的判断逻辑在这里:
if ( bitmpHead.biSize != 40 ) // 如果head大小不对
RclCloseFile(2176u, (int)&dword_71C42F48);
也就是说,VC6 的资源编译器在处理 ICO 文件时,会读取每个图像数据块的 BITMAPINFOHEADER 结构,并检查其 biSize 字段是否等于 40。如果不等于 40,就会报出错误 RC2176。
那么,我们这个报错的 ICO 文件中,为什么 biSize 不是 40 呢?来调试一下看看实际读取到的值。


可以看到,这个值并不是0x28(40),而是 0x474E5089,说明这里并不是一个BITMAPINFOHEADER结构。
如果对常见图片文件的格式有所了解的话,会发现这里真实的值其实是 PNG 文件的魔数(PNG 文件的前 8 个字节是固定的:89 50 4E 47 0D 0A 1A 0A)。
两种图标存储格式的区别
其实之前我对ico文件格式也不是特别了解,通过查阅资料才知道,现代的 ICO 文件格式允许使用 PNG 格式来存储图标数据,特别是对于高分辨率图标(如 256x256)。
这意味着,在 ICO 文件中,某些图像数据块可能并不是传统的 DIB 格式,而是以 PNG 格式存储的。
我用VS2022打开编译正常的旧MFC图标文件,发现确实是这样的,图像数据块以 BMP 格式存储:


而打开编译时报错的新图标文件,发现图像数据块以 PNG 格式存储:


用winhex分别查看这两个图标文件的二进制内容,结合ICO文件的头部定义,可以清晰地看到区别:




结论
VC6 自带的资源编译器(rc.exe)只能识别标准的 BMP (DIB - Device Independent Bitmap) 格式存储的图标数据,而不能处理以 PNG 格式存储的图标数据。当你的 ICO 文件中包含了 PNG 格式存储的图像数据时,VC6 无法解析该头部信息,从而误报为 "old DIB"(旧式 DIB)错误。
实际上,是从 Windows Vista 开始,ICO 文件格式才引入了对 PNG 格式存储图标数据的支持,而 VC6 早在 1998 年发布,远早于这一标准的制定,所以无法兼容这种新格式。网上有一些误导性的说法,认为是图标尺寸过大(如 256x256)导致的问题,实际上并非如此。VC6 是可以支持大尺寸图标的,只要这些图标以 BMP 格式存储即可。
解决办法
现在我们已经明确了问题的根源,要解决这个问题,有两种思路可以考虑:
1、将 ICO 文件转换为全 BMP 存储格式。
2、修改VC6 的 rc.exe识别逻辑以支持 PNG 格式的图标数据。
方法一:转换 ICO 文件为全 BMP 存储格式
网上有很多图标编辑工具可以实现这一转换,比如 IcoFX、Greenfish Icon Editor Pro 等。我没有去测试这些软件,直接用python的PIL库写了个小脚本,把PNG格式的图标转换成BMP格式的图标,代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pure_ico.py
完全手写 ICO 容器,把多张 24-bit BMP 位图打包进单个 .ico
用法:
python pure_ico.py input.png output.ico
"""
import struct
import sys
import os
from PIL import Image
# 需要生成的图标尺寸
SIZES = (16, 32, 256)
def build_bmp24_header(width: int, height: int) -> bytes:
"""
拼一张 24-bit 自下而上 DIB/BMP 信息头(无文件头)。
返回:位图信息头 + 像素数据(BGR 24-bit,4 字节对齐)
"""
w, h = width, height
row_size = ((w * 3 + 3) // 4) * 4 # 每行字节数,4 字节对齐
data_size = row_size * h
header_size = 40 # BITMAPINFOHEADER
# BITMAPINFOHEADER
hdr = struct.pack('<IIIHHIIIIII',
header_size, w, h * 2, 1, 24, 0, data_size, 0, 0, 0, 0)
return hdr, row_size
def img_to_bmp24_bytes(im: Image.Image, size: int) -> bytes:
"""把一张 RGB 图缩放到指定尺寸,返回 24-bit BMP 位图数据(无文件头)"""
im = im.convert('RGB').resize((size, size), Image.LANCZOS)
w, h = im.size
hdr, row_size = build_bmp24_header(w, h)
# 像素区:自下而上、BGR
raw = bytearray(row_size * h)
pixels = list(im.getdata())
for y in range(h):
src_offset = (h - 1 - y) * w
dst_offset = y * row_size
for x in range(w):
r, g, b = pixels[src_offset + x]
raw[dst_offset + x * 3] = b
raw[dst_offset + x * 3 + 1] = g
raw[dst_offset + x * 3 + 2] = r
return hdr + raw
def build_ico(entries: list[tuple[int, bytes]]) -> bytes:
"""
把 [(size, bmp24_data), ...] 打包成完整 ICO 文件
"""
num = len(entries)
# ICONDIR
ico = bytearray()
ico.extend(struct.pack('<HHH', 0, 1, num)) # Reserved, Type=1, Count
# 计算每个 ICONDIRENTRY 的偏移
offset = 6 + num * 16
data_chunks = []
for size, bmp24 in entries:
width = size if size < 256 else 0 # 0 代表 256
data_len = len(bmp24)
# ICONDIRENTRY
ico.extend(struct.pack('<BBBBHHII',
width, 0, 0, 0, 1, 24, data_len, offset))
data_chunks.append(bmp24)
offset += data_len
# 追加所有位图数据
for chunk in data_chunks:
ico.extend(chunk)
return bytes(ico)
def img_to_ico(src_path: str, ico_path: str) -> None:
with Image.open(src_path) as im:
if im.mode != 'RGB':
im = im.convert('RGB')
entries = []
for size in SIZES:
bmp24 = img_to_bmp24_bytes(im, size)
entries.append((size, bmp24))
ico_data = build_ico(entries)
with open(ico_path, 'wb') as f:
f.write(ico_data)
print(f'✅ 已生成 {ico_path},内含尺寸:{SIZES},全部 24-bit 无压缩 BMP')
# 输出文件大小
file_size = os.path.getsize(ico_path)
print(f'文件大小: {file_size} 字节')
if __name__ == '__main__':
if len(sys.argv) != 3:
print('用法: python pure_ico.py <输入图片> <输出.ico>')
sys.exit(1)
img_to_ico(sys.argv[1], sys.argv[2])
运行效果如下:


用VS2022打开生成的图标文件,发现图像数据块确实是以 BMP 格式存储的:


用这个图标替换原来VC6的默认图标,通过,没有再报错,并且图标显示正常。
缺点嘛,就是生成的图标文件体积变大了。
我用两个图片转换成两种格式的图标测试了一下(包含16x16、32x32、256x256三种大小):


可以看到,bmp存储格式的大小是固定的,而png存储格式的大小则小很多,相差可以达到数倍。如果你不在乎图标文件大小的话,这个方法是最简单直接的。
方法二: 修改 VC6 的 rc.exe 识别逻辑以支持 PNG 格式的图标数据
具体来说,就是在判断 biSize 字段的地方,增加对 PNG 魔数的识别。如果检测到是 PNG 格式的数据,就按照PNG文件格式提取图像元数据,并 允许继续处理该图像数据块,以便正确地将其包含在资源文件中。
理论上这是可行的,但实际上在没有源码的情况下修改 RCDLL.dll的二进制文件是非常复杂且容易出错的,就像是一台精密的外科手术,颇有一定难度。
很巧的是,我刚好在电脑上搜索RCDLL.dll时,发现在目录C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin还有一套rc.exe和rcdll.dll,这个版本是6.1.7600.16385,确认了一下是VS2010带的一套工具,明显比VC6自带的5.0.1473.1要新一些。那么问题来了,这套工具能不能替代VC6自带的工具来使用呢?于是我尝试把这两个文件直接拷贝到VC6的安装目录下覆盖原有文件,然后重新编译测试项目,结果成功了!没有再报错,说明这个版本的rc.exe和rcdll.dll已经支持PNG格式的图标数据了,编译后程序经测试也可以正常使用大图标。


既然如此,这可比修改RCDLL.dll简单多了,推荐大家使用这种方法。我把这两个文件打包放在网盘了,有需要的可以直接下载使用。
下载地址: https://pan.baidu.com/s/1-wdrPTXc0D408D4BfflJ3Q?pwd=d5qu 提取码: d5qu
总结
通过对 VC6 资源编译器的深入分析,我们发现其报错的根本原因是无法处理以 PNG 格式存储的图标数据,导致报出 "old DIB" 错误。建议的解决方法是使用更新版本的资源编译工具,或者将 ICO 文件转换为全 BMP 存储格式。希望本文能帮助大家解决类似的问题,顺利编译老项目!

浙公网安备 33010602011771号