NASM 调用 GdipDrawLine 返回成功却不显示直线,如何排查?
应先修正 Graphics 对象的生命周期:每次 WM_PAINT 绘图结束后,先调用 GdipDeleteGraphics,再调用 EndPaint。当前代码直到 WM_DESTROY 才清理对象,每次重绘又会覆盖旧指针,这一点可以从代码中确认。但仅凭 EAX 为 0,还无法确定对象生命周期问题就是窗口空白的唯一原因。
为什么返回成功仍然可能看不到直线?
GDI+ 返回 Ok 表示该调用没有报告错误,绘图结果仍可能位于当前可见区域之外。例如,内容完全位于裁剪区域之外时,调用也可能成功。
检查时应在函数返回后立即查看 EAX,并区分不同 API 的返回值:
GdipCreateFromHDC、GdipCreatePen1、GdipDrawLine返回 GDI+ 状态码,0 表示Ok。BeginPaint返回 HDC,成功时应为非空句柄;如果返回 0,需要先处理这个失败。- 创建函数将对象指针写入
graphicsPtr、penPtr,不通过 EAX 返回。
因此,不能根据“每条指令执行后 EAX 都是 0”判断所有调用都成功。
先把对象清理移到 EndPaint 之前
原代码的执行顺序是:
BeginPaint
创建 Graphics
创建 Pen
绘制直线
EndPaint
……直到窗口销毁才删除 Graphics 和 Pen
通过 HDC 创建的 Graphics 必须在释放该设备上下文之前删除,这是 Microsoft 对此类对象规定的生命周期要求。参见 Graphics::FromHDC 文档。
在现有绘图部分,清理顺序至少应调整为:
invoke GdipDrawLine, dword [graphicsPtr], dword [penPtr], dword [x1], dword [y1], dword [x2], dword [y2]
invoke GdipDeletePen, dword [penPtr]
mov dword [penPtr], 0
invoke GdipDeleteGraphics, dword [graphicsPtr]
mov dword [graphicsPtr], 0
invoke EndPaint, dword argv(gdiphWin), ps
这个片段展示的是成功路径。实际代码还需要在创建失败时跳过后续绘图,并清理已经创建的对象。原代码每次绘制都会创建新的 Graphics 和 Pen,却只保存最后一组指针,因此还存在对象泄漏。
带失败处理的 NASM 示例
下面的例程沿用原代码中的变量和 invoke 宏,适用于当前 -f win32 构建。它显式建立并恢复栈帧,可以替换原来的整个 GDIPaint 例程。不要再给这段例程套用原来的 proc、endproc 或额外的返回代码。
先在 .data 段增加一个调试变量:
paintStatus dd 0
替换例程如下:
GDIPaint:
push ebp
mov ebp, esp
mov dword [graphicsPtr], 0
mov dword [penPtr], 0
mov dword [paintStatus], 0
invoke BeginPaint, dword [ebp + 8], ps
mov [hDC], eax
test eax, eax
jz .end_paint
invoke GdipCreateFromHDC, dword [hDC], graphicsPtr
mov [paintStatus], eax
test eax, eax
jnz .cleanup
; UnitPixel = 2
invoke GdipSetPageUnit, dword [graphicsPtr], 2
mov [paintStatus], eax
test eax, eax
jnz .cleanup
; Use a 2.0-pixel pen for this diagnostic example.
invoke GdipCreatePen1, dword [penColor], dword [penWidth], 2, penPtr
mov [paintStatus], eax
test eax, eax
jnz .cleanup
invoke GdipDrawLine, dword [graphicsPtr], dword [penPtr], dword [x1], dword [y1], dword [x2], dword [y2]
mov [paintStatus], eax
.cleanup:
cmp dword [penPtr], 0
je .delete_graphics
invoke GdipDeletePen, dword [penPtr]
mov dword [penPtr], 0
.delete_graphics:
cmp dword [graphicsPtr], 0
je .end_paint
invoke GdipDeleteGraphics, dword [graphicsPtr]
mov dword [graphicsPtr], 0
.end_paint:
invoke EndPaint, dword [ebp + 8], ps
mov dword [hDC], 0
xor eax, eax
mov esp, ebp
pop ebp
ret 4
paintStatus 保存最近一次执行的创建、设置或绘制操作的状态,避免被清理调用覆盖。调试时仍应在各调用后的状态检查处设置断点。如果 BeginPaint 失败,应直接检查它返回的 HDC,不能用 paintStatus 判断。
这个示例尚未在原运行环境中编译验证,invoke 的具体展开取决于使用的头文件和宏定义。
检查初始化与退出顺序
GdiplusStartup 必须在任何其他 GDI+ 调用之前成功执行,所有 GDI+ 对象也必须在 GdiplusShutdown 之前销毁。参见 GdiplusStartup 文档。
原代码先创建带 WS_VISIBLE 的窗口,再初始化 GDI+。建议在创建窗口之前完成初始化并立即检查返回值,以免窗口创建期间的消息处理提前进入绘图路径。这个调整是为了保证调用顺序,不能据此断言原程序已经发生了提前绘图。
按上面的示例在每次绘图后清理对象后,删除 WM_DESTROY 中原有的两条无条件对象删除调用,保留与成功初始化配对的 GdiplusShutdown。如果还保留其他长期存在的 GDI+ 对象,应先清理它们。
核对单位、参数和裁剪范围
画笔单位与坐标单位分别设置
原代码中的 GdipSetPageUnit(..., 2) 使用 UnitPixel,而 GdipCreatePen1(..., 6, ...) 使用 UnitMillimeter,因此画笔宽度是 2 毫米。示例将画笔单位也改为 2,排查时使用 2 像素宽的线。单位含义可查阅 Unit 枚举文档。
单位不一致会影响线宽,但仅凭这一点无法解释完全没有输出。颜色 0xFF0000FF 的 Alpha 为 255,画笔也不是透明的。
保留 REAL 参数的浮点位模式
GdipDrawLine 的坐标参数是单精度浮点数。原代码使用:
x1 dd 10.0
再通过 dword [x1] 传参,会把浮点数的 32 位位模式压入栈中,符合该参数的表示方式。不要直接改成整数立即数 10;若要使用整数坐标接口,应另行使用 GdipDrawLineI。
确保更新区域覆盖直线
BeginPaint 会把绘图裁剪到窗口的更新区域。如果该区域没有覆盖从 (10, 10) 到 (100, 200) 的线段,即使绘制成功,也可能看不到变化。可以在初始化成功、窗口创建完成后触发一次完整客户区重绘:
invoke InvalidateRect, dword [hWnd], NULL, TRUE
invoke UpdateWindow, dword [hWnd]
这些调用应放在 WM_PAINT 处理之外,避免不断触发重绘。更新区域与裁剪行为参见 BeginPaint 文档。
仍然空白时如何继续定位?
修正上述顺序后,依次检查 BeginPaint 返回的 HDC、创建函数写入的对象指针、每个 GDI+ 调用的即时返回值,以及实际传入的浮点参数。
如果需要排查待完成的绘图操作,可以在成功绘制之后、删除 Graphics 之前临时加入:
; FlushIntentionSync = 1
invoke GdipFlush, dword [graphicsPtr], 1
使用前需要确认头文件声明了 GdipFlush。同步刷新会等待待处理绘图操作完成,但不能代替正确清理对象,也不能保证解决所有空白问题。其同步语义参见 FlushIntention 文档。
最后检查 proc、ret、endproc 的宏展开和调用前后的 ESP。输入中没有提供这些宏定义,末尾也未展示 endproc,因此无法仅凭这段文本判断原来的 ret 4 是否正确恢复了栈帧。即使操作系统是 64 位,当前汇编命令生成的也是 32 位目标文件,不需要因此改用 x64 调用约定。
备注:内容仅供参考。
