setupldr!SlPositionCursor 与 TextSetCursorPosition 函数原理分析:从 VIDEO_BUFFER_VA 推导光标位置公式

发布时间:2026/9/30 22:26:34
setupldr!SlPositionCursor 与 TextSetCursorPosition 函数原理分析:从 VIDEO_BUFFER_VA 推导光标位置公式 1. 从一次蓝屏前的光标跳动说起setupldr 光标定位到底在做什么如果你曾经用 WinDbg 双机调试过 Windows 启动阶段大概率见过setupldr!SlPositionCursor这个符号。它出现在启动加载器把状态文字写到屏幕右下角的那一刻比如经典的Setup is loading files...或者进度提示。很多人第一次看到它会以为这只是个简单的“移动光标”函数但真正跟进去会发现它背后牵扯到 ARC 固件抽象层、ANSI 转义序列解析、以及最底层的 VGA 文本显存寻址。这篇文章要解决的核心问题很具体setupldr 里 SlPositionCursor 和 TextSetCursorPosition 是怎么配合的以及光标行列坐标如何通过 VIDEO_BUFFER_VA 换算成显存偏移。适合正在做启动阶段调试、逆向分析 bootmgr/setupldr、或者单纯想搞懂 VGA 文本模式显存布局的读者。你不需要有内核开发经验只要能看懂 C 和基本汇编跟着调试器一步步走就行。先给结论后面再展开推导Vp (PUCHAR)(VIDEO_BUFFER_VA (Row * VIDEO_BYTES_PER_ROW) (2 * Column));其中VIDEO_BUFFER_VA 0xb8000VIDEO_BYTES_PER_ROW 80 * 2 0xa0。这个公式就是整个光标定位的数学核心。但光有公式不够你得知道Row和Column是从哪来的、经过了哪些转换、为什么最后落到TextTmPositionCursor里才真正算出Vp。调用链大致是这样的SlPositionCursor(x, y) - 拼 ANSI 字符串 \033[y1;x1H - ArcWrite - AEWrite - AEWriteEx - BiosConsoleWrite - 解析转义序列提取 Parameter[0]y1, Parameter[1]x1 - TextSetCursorPosition(Parameter[1]-1, Parameter[0]-1) - TextTmPositionCursor(Row, Column) - Vp VIDEO_BUFFER_VA Row*0xa0 2*Column我第一次跟这条链的时候最困惑的是明明SlPositionCursor(2, ScreenHeight-1)传的是 x2、y24为什么中间要绕一圈 ANSI 转义序列直接算偏移不行吗后来才明白这是 ARC 固件抽象层的设计——上层不关心底层是 VGA 还是串口终端统一用 ANSI 序列表达“把光标移到某处”由BiosConsoleWrite负责解释。理解了这层抽象整条链就顺了。下面我会先讲清楚问题场景和数据结构再给出可复制的反汇编与调试命令然后推导公式最后把常见报错和排查方法列出来。全程用 WinDbg 的kc、dv、db、eb这些命令验证你可以直接照着敲。2. SlPositionCursor 与 TextSetCursorPosition 的调用关系与数据结构2.1 两个函数的职责边界SlPositionCursor属于 setupldr 的 ARC 显示层arcdisp.c它的参数是屏幕坐标(x, y)语义是“把下一次输出的位置设到第 y 行第 x 列”。注意这里 x 是列、y 是行和常见的(row, col)顺序相反容易搞混。TextSetCursorPosition属于更底层的文本控制层参数是(X, Y)语义是“设置软光标位置”。它内部会调用TextTmPositionCursor(Row, Column)最终算出显存指针Vp。两者的关系可以这样理解SlPositionCursor是“面向 ARC 设备”的接口TextSetCursorPosition是“面向文本显存”的接口中间隔着 ANSI 转义序列的解析。2.2 SlPositionCursor 的宏实现在 setupldr 源码里SlPositionCursor实际上是一个宏ARC_DISPLAY_POSITION_CURSOR#define ARC_DISPLAY_POSITION_CURSOR(x, y) { \ ULONG LocalCount; \ CHAR LocalBuffer[40]; \ sprintf(LocalBuffer, \033[%d;%dH, y 1, x 1); \ ArcWrite(BlConsoleOutDeviceId, LocalBuffer, strlen(LocalBuffer), LocalCount); \ }关键点ANSI 的H命令参数是行;列且从 1 开始计数。所以传入的(x, y)要变成(y1, x1)。比如SlPositionCursor(2, ScreenHeight-1)当ScreenHeight25时y24拼出来就是\033[25;3H。2.3 调试器里看调用栈用kc看栈能清楚看到从SlPositionCursor到BiosConsoleWrite的层次kd kc 9 # 00 setupldr!AEWriteEx 01 setupldr!AEWrite 02 setupldr!SlPositionCursor 03 setupldr!SlWriteStatusText 04 setupldr!BlOutputLoadMessage 05 setupldr!SlInit 06 setupldr!BlStartup 07 setupldr!NtProcessStartup在SlPositionCursor帧里用dv看局部变量kd dv x 2 y 0x18 LocalCount 2 LocalBuffer char [40] ???y 0x18 24x 2和SlPositionCursor(2, ScreenHeight-1)完全对上。LocalBuffer里就是拼好的 ANSI 字符串。2.4 BiosConsoleWrite 里的转义序列解析继续跟到BiosConsoleWrite用db看缓冲区内容kd db 0x000609f0 000609f0 1b 5b 32 35 3b 33 48 00-00 00 00 00 00 00 00 00 .[25;3H.........1b是 ESC5b是[32 35是253b是;33是348是H。解析过程分三步第一步1b触发EscapeSequence TRUEcase ASCI_ESC: EscapeSequence TRUE; break;第二步5b触发ControlSequence TRUE并初始化参数if (*String [) { ControlSequence TRUE; PCount 0; Parameter[0] 0; }第三步数字字符累加到Parameter[PCount]遇到;就PCountif ((*String 0) (*String 9)) { Parameter[PCount] Parameter[PCount] * 10 *String - 0; continue; } switch (*String) { case ;: PCount; if (PCount CONTROL_SEQUENCE_MAX_PARAMETER) { PCount CONTROL_SEQUENCE_MAX_PARAMETER; } Parameter[PCount] 0; break;解析完25;3H后Parameter[0]0x1925Parameter[1]0x33。然后遇到Hcase H: TextSetCursorPosition(Parameter[1]-1, Parameter[0]-1); ControlSequence FALSE; break;注意参数顺序Parameter[1]-1是 X列Parameter[0]-1是 Y行。所以TextSetCursorPosition(2, 24)。2.5 TextSetCursorPosition 到 TextTmPositionCursor在TextSetCursorPosition帧里dvkd dv X 2 Y 0x18再进一层到TextTmPositionCursorkd dv Row 0x18 Column 2到这里Row24、Column2终于可以套公式了。3. 可复制配置VIDEO_BUFFER_VA 与光标偏移公式推导3.1 VGA 文本模式显存布局VGA 文本模式 80x25每个字符占 2 字节低字节是 ASCII 码高字节是属性前景/背景色。所以一行 80 列占 160 字节即0xa0。显存基址0xb8000对应第 0 行第 0 列。相关宏定义#define VIDEO_BUFFER_VA 0xb8000 #define VIDEO_ROWS 25 #define VIDEO_COLUMNS 80 #define VIDEO_BYTES_PER_ROW (VIDEO_COLUMNS * 2)3.2 TextTmPositionCursor 的完整实现VOID TextTmPositionCursor( USHORT Row, USHORT Column ) { if (Row VIDEO_ROWS) { Row VIDEO_ROWS - 1; } if (Column VIDEO_COLUMNS) { Column VIDEO_COLUMNS - 1; } Vp (PUCHAR)(VIDEO_BUFFER_VA (Row * VIDEO_BYTES_PER_ROW) (2 * Column)); }边界检查很重要如果Row 25就钳到 24Column 80就钳到 79防止越界写到显存之外。3.3 公式推导把Row24、Column2代入Vp 0xb8000 24 * 0xa0 2 * 2 0xb8000 0x960 0x4 0xb8f04用 WinDbg 验证kd ?b800018*a02*2 Evaluate expression: 757508 000b8f04和dv看到的Vp 0x000b8f04完全一致。3.4 属性字节的验证每个字符的第二字节是属性。默认白底黑字是0x70红底白字是0x47。用eb改属性kd eb 0xb8000a012*0 47 kd eb 0xb8000a012*1 47 kd eb 0xb8000a012*2 47再用db看kd db 0xb8000a0 000b80a0 57 47 69 47 6e 47 64 47-6f 47 77 47 73 47 20 47 WGiGnGdGoGwGsG G 000b80b0 53 47 65 47 74 47 75 47-70 47 20 17 20 17 20 17 SGeGtGuGpG . . .可以看到W的属性从0x17变成了0x47说明偏移计算正确。3.5 一份可直接复用的结构体与常量如果你要在自己的调试脚本里复现这套计算可以把这些常量固化下来typedef struct _VIDEO_TEXT_CONTEXT { ULONG VideoBufferVa; // 0xb8000 ULONG BytesPerRow; // 0xa0 ULONG Rows; // 25 ULONG Columns; // 80 } VIDEO_TEXT_CONTEXT; static VIDEO_TEXT_CONTEXT gVideoCtx { 0xb8000, 0xa0, 25, 80 }; PUCHAR ComputeCursorAddress(ULONG Row, ULONG Column) { if (Row gVideoCtx.Rows) { Row gVideoCtx.Rows - 1; } if (Column gVideoCtx.Columns) { Column gVideoCtx.Columns - 1; } return (PUCHAR)(gVideoCtx.VideoBufferVa Row * gVideoCtx.BytesPerRow 2 * Column); }这段代码和TextTmPositionCursor的逻辑一一对应可以直接嵌到你的调试辅助工具里。4. 验证请求用 WinDbg 逐步确认光标位置公式4.1 设置断点在TextTmPositionCursor下断kd bp setupldr!TextTmPositionCursor kd g命中后看参数kd dv Row 0x18 Column 24.2 单步到 Vp 赋值用p单步观察Vp的变化kd p kd p kd dv Vp 0x000b8f044.3 手工计算对照kd ?b800018*a02*2 Evaluate expression: 757508 000b8f04两者一致公式验证通过。4.4 验证 ANSI 解析的中间状态在BiosConsoleWrite里可以用x看全局变量kd x setupldr!EscapeSequence 00343889 setupldr!EscapeSequence 0x00 kd x setupldr!ControlSequence 00343888 setupldr!ControlSequence 0x00初始都是 0。执行到1b后kd x setupldr!EscapeSequence 00343889 setupldr!EscapeSequence 0x01执行到5b后kd x setupldr!ControlSequence 00343888 setupldr!ControlSequence 0x01再看Parameter数组kd dx -r1 (*((setupldr!unsigned long (*)[10])0x34f340)) [0] : 0x19 [1] : 0x1 [2] : 0x2f解析完25;3H后[0] : 0x19 [1] : 0x3Parameter[0]25、Parameter[1]3和预期一致。4.5 验证显存写入用db看Vp指向的位置kd db 0xb8f04 000b8f04 20 17 20 17 20 17 20 17-20 17 20 17 20 17 20 17 . . . . . . . .这里是空格加属性0x17说明光标位置已经定位到第 24 行第 2 列。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照虽然这篇是启动加载器逆向但很多读者是在做 AI 辅助调试或远程调试环境时遇到问题才搜到这里的。下面把几类高频报错和本篇场景对照一下。5.1 401 Unauthorized如果你在用某个远程调试代理或 API 转发工具遇到 401通常是 Key 没配或过期。检查你的配置文件里api_key字段是否正确。对于本地 WinDbg 调试401 一般不会出现除非你用了网络调试通道。5.2 local proxy failed这个报错常见于本地代理进程没起来或者端口被占用。排查步骤先确认代理进程在运行再确认监听端口和客户端配置一致。如果是 WinDbg 的kd连接检查com口或net端口设置。5.3 reading choices 报错这类报错通常出现在调用模型接口时返回体里choices字段解析失败。原因可能是返回了错误结构或者流式响应被截断。对照到本篇类似BiosConsoleWrite解析转义序列时如果Parameter数组越界也会出现“读到的内容不符合预期”的情况。检查PCount是否超过CONTROL_SEQUENCE_MAX_PARAMETER。5.4 OAuth 相关报错OAuth 报错多见于需要令牌刷新的场景。如果你在配置调试工具链时遇到先确认令牌有效期再确认回调地址是否匹配。和本篇的 ANSI 解析类似OAuth 也是一套“状态机 参数解析”的流程任何一步状态没对上都会失败。5.5 光标位置算错的典型表现如果你自己实现公式时算错了典型表现是文字写到错误行、属性字节错位、或者屏幕出现乱码。排查方法先用?命令手工算一遍再和dv看到的Vp对照。常见错误包括忘了*2每字符 2 字节、把Row和Column搞反、VIDEO_BYTES_PER_ROW写成 80 而不是 160。5.6 断点打不上的情况如果bp setupldr!TextTmPositionCursor提示符号找不到先确认符号路径配置正确再确认模块已加载。可以用lm看模块列表用x setupldr!*Cursor*搜索符号。6. 从公式到实践把光标定位逻辑用到你的调试流程里搞懂这条链之后最直接的收益是你在调试启动阶段显示问题时能快速定位是 ANSI 解析错了还是显存偏移算错了。比如屏幕文字错位先看Parameter数组对不对再看Vp算得对不对两步就能缩小范围。如果你想把类似能力接到 AI 辅助编码或调试工作流里可以用 TaoToken 的模型对话能力来帮你解释反汇编片段或者用 Coding Plan 做长期的启动阶段代码分析。接入时把 Base URL、Key、Model ID 三件套配好即可{ base_url: https://taotoken.net/api, api_key: 你的Key, model_id: claude-sonnet-4-5 }需要生成 Key 的话去 API Keys 页面接入细节看接入文档想先验证模型效果用模型对话长期做启动加载器或 Agent 相关开发用 Coding Plan 更合适。最后留一个实用技巧在 WinDbg 里把?b8000Row*a02*Column存成别名每次直接调用省得手敲。比如kd .dvalloc /w 0x1000 kd as /x CursorAddr b800018*a02*2 kd ? CursorAddr这样你就能在调试会话里快速验证任意行列的显存地址不用每次都重新推导公式。