PAYATHON 2026

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 调用约定。

备注:内容仅供参考。