善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。
显示标签为“Windows”的博文。显示所有博文
显示标签为“Windows”的博文。显示所有博文

2026-05-12

为醋包饺子

儿子在问我存起来的老游戏里面,《战国美少女》是什么游戏?
我告诉他说,是经典的日式 RPG。
他又问我,什么是 RPG,以及什么是「日式 RPG」。
在这之前,他只知道 RPG 是 DF5 里面的「火箭筒」,以及他只玩过《Fallout 4》那种美式 RPG。

于是,我打算把游戏拿出来,当场演示给他看。
然后,我发现,在我笔记本电脑的 Win11 上已经玩不了这游戏了。于是我把 VMware 找来,我有一个 WinXP 虚拟机,就是为这种事情准备的。
再然后,我发现我的 WinXP 虚拟机启动很慢,进桌面花的时间比我预期的要长得多,声音也是发抖的。然而游戏还是跑不起来。儿子一看「这么麻烦」,转身就溜了,留下我独自发愁。

WinXP 虚拟机的这个情况,我是知道的。之前在公司办公电脑上也遇到过。当时是开了 WSL2,然后 VMware 就多出来了一个选项。

图片来自网络

对于 Win7 以及更高版本的虚拟机而言,这个选项是有效果的,勾上就基本能解决问题了。但是 WinXP 虚拟机不行,勾上还是很慢。偏偏 BCB5 在 XP 下面活得比较好,各方面都比较舒服。有的同事面对这种问题是换到 Win7 虚拟机下去解决的,我最后是把 WSL2 给关掉了,反正现在用不上。

我这次也想把笔记本电脑上的 Hyper-V 给关掉。怎么关我也知道,但笔记本电脑上是 Win11 家庭版,通常的方式行不通。BIOS 里面看了一下,没有相关的信息,于是去问 Google。也有人在咨询这个问题,答案不一而足,但试了也没用。

其实我看这个「家庭版」不爽很久了。
买回来笔记本的当天,我就纠结了蛮长的时间。当时有在三个选项中来回权衡很久:

  1. 把笔记本重装成 Win10 专业版:我最熟,用起来肯定最顺手。当时我公司电脑和家里 HTPC 都是 Win10 专业版,之前的笔记本也是。这台新笔记本是我第一次用 Win11。然而,始终缩在「过去」总不是一个事。
  2. 把笔记本直接重装成 Win11 专业版:专业版解决了 RDP 的问题。然而原始出厂的东西,预安装的那些就统统没了。我知道上面是有一个 Office 的。当时我还不知道 Windows 的 审核模式 这个东西。而且我担心重装 OS 之后我的质保会出问题。
  3. 保留现在的 Win11 家庭版先用,以后再说:最后我选择了这个在我看来有点憋屈,但最稳健的做法。

现在这台笔记本电脑我已经用了两年多了,整机质保已经过了。主要硬件也扛到了现在,质保对我而言没什么大用处了,而「代价」现在慢慢开始浮现。家庭版没法开 RDP,并且现在 Hyper-V相关的东西也开关不了。但我现在上面东西也比较多,重装肯定是不希望的。那既然微软可以让人付费升级,我有没有办法呢?

问了 Google Gemini,答案是有。那就是连微软自己的技术支持人员也在用的 MAS
用 MAS,你可以把 Windows 版本给变掉。然后 Windows 会变成未激活。接下来怎么办呢?都用上 MAS 了,就别问我这个了。

Win11 专业版下,Hyper-V 可以关掉了。然而我去看的时候,发现本来就是关着的。别的相关内容也没有开。所以「家庭版」还真是干净的。我只是收获了 RDP 而已。
去看了看VMware,发现选项还在。按照 Google Gemini 说的把「内存隔离」给关掉,也没有用。我知道如果 Hyper-V 真的关掉了,那个选项会消失的。所以还是没能成功。

最后我是照着 这个 处理的,看不懂英文版的可以看 中文版,Reddit 的翻译功能还挺不错的,有时候蛮接地气。关键是第 6 步,完事后第一次重启之后黑了屏,吓得我不轻,好在之后无大碍。

重启完之后,我看了一下相关的变化。发现「固件隔离」是被关掉了。可能刚才我最后差的就是这一步吧。
现在 VMware 里面没有那个 Hyper-V 相关的选项了,我的 WinXP 虚拟机,运行性能也终于正常了。

然而我的《战国美少女》还是玩不了。
弹的报错框是 Big5 码的内容。截了图发给 Google Gemini 看了一下,说是路径有问题。
奇怪我记得我以前是玩过的,当时应该没有遇到类似问题。试了一下,手上的《战国美少女2:春风之章》可以直接在 Win10 上玩。
折腾了一大圈,解决了一堆问题,竟然又回到了起点。好吧,算了算了,去网上找了个百度云盘的 资源 补上。有空再给儿子演示看看吧。

此致,敬礼。

2026-04-08

当 AI 遇上忒修斯之船

在软件中需要加入一些为用户个性化定制的内容推送,因此需要一个用户标识。

正常情况下,如果用户有登录,就是一个 UserID 的事情。然而我们这次的场景是匿名使用,所以没有这个 UserID 可以用,需要自己来生成一个。

以及,产品设计上希望能做到还是能大致识别到一个「人」。同一台机器上如果有多个我们的软件,最好能得到相同的 ID。因此最后商量下来决定采用设备 ID(或设备指纹)的方式。

如果是 Web 浏览器,算是有相对成熟的方案。浏览器指纹也不是一两个人在做了,「广告」这个需求天然就需要这种东西。然而我们是 MFC 程序,需要自己想办法。

我以前做 Shareware 的时候也大致接触过一些 DeviceID 的东西,知道这种事情吃力不讨好。很难覆盖那么多种类的硬件,特别有一些可能还是服务器上才能见到的东西。万一搞不好,说不定在某些特殊硬件上还要出什么状况,所以并不是一件容易的事情。所幸我们这次的事情并不是性命攸关的「注册码」,只是一个可有可无的身份标识,即使拿不到,也可以接受,因此压力没那么大。

很自然地,我想到了让 AI 来做这个事情。说到「见多识广」,可能没有人比得上它。知识结构也是它比较新,不用担心去网上找到的开源代码只能支持老旧硬件的事情。把需求描述给了它,很快就生成了一个函数,专门用来在 Windows 上得到 DeviceID。

自测的时候,问题来了:DeviceID 有时候会变。
其实这个问题一早就埋下了,是我需求没向AI说清楚,算我的锅。

说起来,这类需求虽然都可以描述为 DeviceID,但实际上是不同的:

  1. 用于区分两台设备;
  2. 用于追踪使用者。

两者是有大区别的。前一种就最好是有点变化就换个 ID,后一种则应该只要怀疑可能还是原来那台,那 ID 就不要动。

但是,这种事情确实不太好把握。加根内存条算不算新设备?可能不应该算。那换块硬盘呢?如果 CPU 升了个级,心脏都变了,还说没变化,有点说不过去吧?以及,如果用户把操作系统从正版的 Win10 家庭版给重装成了盗版 Win11 专业版呢?

图片由 Google Gemini 3 Pro + Nano Banana Pro 生成

我们的需求肯定是希望尽可能不要因为一丁点儿变化就换一个 DeviceID。但这事是一个「忒修斯之船」悖论。变到什么程度才算一台新设备?如果太过追求某一个极端,就会落入两头的陷阱。要确保 DeviceID 的「排他性」,就需要冒「稳定性」不佳的风险。

AI一开始给出来的方案,「稳定性」是不够好的。即使硬件和 OS 都没有发生变化,它也可能会变。据说是多核 CPU 的线程绑定带来的问题。麻烦在于,如果我不从需求侧来反推,以及没有进行足够数量的测试,很可能发现不了。

解决了这个问题,还有另外一个问题:如何保证这段用于生成 DeviceID 的代码,在各种硬件上都能稳定运行?

我们的要求很低:如果不能保证生成 DeviceID,那就别生成,或者生成一个可能会重复的,都 OK。这算是允许牺牲 DeviceID 的「稳定性」,来换取更好的「鲁棒性」。这个需求我从一开始就给了 AI,它的确也在代码上作了处理,我有看到。然而,如何保证呢?作更多的测试?

我目前能想到的:在调用的时候套一个 try...catch,不知道有没有用。另一个我知道肯定有用的办法,就是把这个事情扔给一个独立的进程来做,这样应该可以保证万无一失。软件运行架构会需要一点调整。我知道这样去做,AI 呢?你如果只是把它限制在写函数这件事情上的话,它肯定没法告诉你答案。

这种事情,让 Claude Code 来做,能更好吗?如果让 OpenClaw 放开手脚不限费用地去干,它最终能搞定吗?GIGO,如果人想偷懒的话,估计就会很难。

2025-11-11

你可能从未注意过的 MFC 陷阱:模态对话框禁用机制的局限性

在最近的一个软件开发案例中,遇到了不容易处理的情况。

在我们的软件中,有那种一直浮在界面上的非模态对话框,例如那种浮动的工具面板。但是,这种窗口,貌似不会在主窗口弹出模态对话框的时候被 Disable。如果模态对话框是在这个非模态对话框中弹出的,那没有问题。

用 VS2013 和升级到最新的 VS2022 各写了一个测试程序,发现这就是 MFC 的默认逻辑。
在主窗口上放了两个点击后各自会弹出非模态对话框和模态对话框的按钮。先弹出非模态对话框,然后再去弹出模态对话框。此时主窗口被 Disable,无法响应鼠标、键盘消息。但非模态对话框不受影响。

本想靠调整产品设计「容忍」过去。但问题在于,如果在保持模态对话框弹出的情况下,去把非模态对话框先关闭了,那主窗口会被 Enable。此时它跟模态对话框之间都能响应鼠标、键盘消息,效果就好像之前弹出的模态对话框变成了非模态对话框一样。这个时候就会有「后果」了,产品设计再怎么调整,也没法让软件在这种乱了套的情况下还能工作正常。


在网上 Google 了半天,不要说解决方案,连问题都没人提到。讲解模态 / 非模态对话框的文章有一些,但都是很入门的介绍。只是从「使用者」的角度去讲用法谈区别,并没有涉及我遇到的问题。很少从原理角度进行说明,更没有去分析源代码。大概 MFC 现在用的人真的不多了吧?

图片由 Google Gemini 生成

还是得自己动手,在 DoModal() 处下了一个断点,单步跟踪进到 MFC 的代码里面看了一下,就明白了。以下代码来自 VS2013,VS2022 我貌似没有安装 MFC 源代码,但从表现上看二者在这个逻辑上应该相差无几。我们一起来看看 CDialog::DoModal() 到底干了些什么事情吧:

INT_PTR CDialog::DoModal()
{
	// can be constructed with a resource template or InitModalIndirect
	ASSERT(m_lpszTemplateName != NULL || m_hDialogTemplate != NULL ||
		m_lpDialogTemplate != NULL);

	// load resource as necessary
	LPCDLGTEMPLATE lpDialogTemplate = m_lpDialogTemplate;
	HGLOBAL hDialogTemplate = m_hDialogTemplate;
	HINSTANCE hInst = AfxGetResourceHandle();
	if (m_lpszTemplateName != NULL)
	{
		hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
		HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
		hDialogTemplate = LoadResource(hInst, hResource);
	}
	if (hDialogTemplate != NULL)
		lpDialogTemplate = (LPCDLGTEMPLATE)LockResource(hDialogTemplate);

	// return -1 in case of failure to load the dialog template resource
	if (lpDialogTemplate == NULL)
		return -1;

	// disable parent (before creating dialog)
	HWND hWndParent = PreModal();
	AfxUnhookWindowCreate();
	BOOL bEnableParent = FALSE;
	CWnd* pMainWnd = NULL;
	BOOL bEnableMainWnd = FALSE;
	if (hWndParent && hWndParent != ::GetDesktopWindow() && ::IsWindowEnabled(hWndParent))
	{
		::EnableWindow(hWndParent, FALSE);
		bEnableParent = TRUE;
		pMainWnd = AfxGetMainWnd();
		if (pMainWnd && pMainWnd->IsFrameWnd() && pMainWnd->IsWindowEnabled())
		{
			//
			// We are hosted by non-MFC container
			// 
			pMainWnd->EnableWindow(FALSE);
			bEnableMainWnd = TRUE;
		}
	}

	TRY
	{
		// create modeless dialog
		AfxHookWindowCreate(this);
		if (!CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), hInst) && !m_bClosedByEndDialog)
		{
			// If the resource handle is a resource-only DLL, the dialog may fail to launch. Use the
			// module instance handle as the fallback dialog creator instance handle if necessary.
			CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), AfxGetInstanceHandle());
		}

		m_bClosedByEndDialog = FALSE;
	}
	CATCH_ALL(e)
	{
		TRACE(traceAppMsg, 0, "Warning: dialog creation failed.\n");
		DELETE_EXCEPTION(e);
		m_nModalResult = -1;
	}
	END_CATCH_ALL

	if (bEnableMainWnd)
		pMainWnd->EnableWindow(TRUE);
	if (bEnableParent)
		::EnableWindow(hWndParent, TRUE);
	if (hWndParent != NULL && ::GetActiveWindow() == m_hWnd)
		::SetActiveWindow(hWndParent);

	// destroy modal window
	DestroyWindow();
	PostModal();

	// unlock/free resources as necessary
	if (m_lpszTemplateName != NULL || m_hDialogTemplate != NULL)
		UnlockResource(hDialogTemplate);
	if (m_lpszTemplateName != NULL)
		FreeResource(hDialogTemplate);

	return m_nModalResult;
}

看到高亮的代码应该就能明白了。MFC 在弹出模态对话框的时候,只是把 hWndParent 给 Disable 了。然后如果发现主窗口还没被 Disable,再补上一刀。后面这个逻辑应该就是为了应对在非模态对话框中弹出模态对话框的情况。

看到了这些逻辑,就可以明白本文最开始讲到的情况是情理之中的。模态对话框只管住了大 BOSS 和父亲,对于兄弟和叔伯之类,都没有去管。如果情况复杂一点,例如非模态对话框中又弹了第二层的对话框,那么不管这个对话框是模态还是非模态,接下来都有机会整出点问题。


知道了问题的原因,接下来就要寻找靠谱的解决方案了。肯定有人解决过这类问题,因为有些软件没有这种问题。但不知道是因为觉得问题太简单了不值得说,还是想藏着掖着?

希望是前者。因为真正的解决方案确实也很简单。

之前有同事尝试解决这个问题,办法是让模态对话框去通知所有的非模态对话框(或者说需要被 Disable 的窗口)自行 Disable,用了我们软件内部与 Windows 消息相互独立的另外一套通讯机制。

但这个解决方案有一些问题:模态对话框除了自己开发的那些,还有系统提供的 MessageBox、文件打开对话框等,另外还包括第三方库中的那些,都是我们无力触达的。所以虽然它花了很多力气让对话框们去多重继承一个基类,但还是没法彻底解决这类问题。

回头想想,主窗口不是总是会被 Disable 的么?那么监听主窗口的 WM_ENABLE 消息,在事件处理函数中把那些需要额外 Disable 掉的窗口集中处理掉,不是就可以了?

实际做起来也很简单,基本上就是在主窗口的 OnEnable() 中用 EnableWindow(bEnable) 把状态传递过去。唯一需要动点脑筋的,就是让那些需要额外 Disable 掉的窗口把自己的 HWND 注册到一个主窗口拿得到的地方,也就是说待处理的窗口列表需要管理起来。

具体的实现代码我就不贴了。重要的是解决问题的思路,具体如何实现,可以有很多种办法,选择适合自己的哪一种就好。相信读到这里的,都是合格的 Win32 / MFC 程序员。

2025-08-05

是谁动了我的 HDC?

最近工作上倒是查了不少问题,然而到了末尾都发现只是一些低级错误,完全不好意思拿出来说。但今天遇到一个,还算有一些意思,可以讲讲。


问题的表现是我们的软件上某些部位偶尔会「花」掉,显示的是之前覆盖在上面的内容。有经验的程序员一看就知道是 GDI 的问题,但到底是什么问题呢?

图片由 Google Gemini 生成

刚开始的时候,以为是 GDI 泄漏了。曾经有过一个例子,漏到了好几千,后来到了 1 万,触发了 CEF 的 CollectGDIUsageAndDie 被 Dump 了。还没到 1 万的时候,界面上的表现就是开始花,其实就是有部分 GDI 函数已经开始调用失败了。

然而这次并不是,GDI 对象数很正常。开发人员远程调试跟了一下,发现是 HDC 拿不到。CreateCompatibleDC() 得到了一个 NULL,因此 MemDC 创建不出来。


什么情况下 CreateCompatibleDC() 会调用失败呢?GDI 对象数不多,内存也很充足。软件中其它部位的 MemDC 是能正常创建的,因此全局性的因素都可以排除。什么光栅设备、显卡驱动之类也就不可能是原因了。
除此之外,就只剩下了一个可能性:CreateCompatibleDC() 传进去的 HDC 参数有问题。

试了一下,乱传一个不存在 HDC,的确会导致 CreateCompatibleDC() 返回 NULL。如果传的是个 NULL 进去,倒是还好一点。看来是窗口上的 HDC 有问题,当然也有可能是窗口本身就有问题。然而其它地方也在用同一个 HWND 创建 MemDC,一切正常。所以还是某些 HDC 有问题。

HDC 的值看起来并不奇怪,也没有什么好办法确认它到底有什么问题。HWND 还能用 Spy++ 来看,HDC 我是一筹莫展。还好可以 OutputDebugString。日志打出来,有意思的地方来了。

我看出问题了:当某个 HDC 失效的时候,它一定是被连续拿到过两次,中间没有 ReleaseDC() 过。这两次 GetDC(),都得到了同一个 HDC,肯定不正常。
我把 this 指针和 HWND 的值也加到了日志里面,这下看得更清楚了。两次 GetDC() 分别是不同的 HWND。而 HDC 失效之时,就是当它被最终 ReleaseDC() 的时候。只不过这次 ReleaseDC() 的 HWND 是一个旧的窗口,那个窗口此时应该已经被销毁了。


窗口 OnDestroy() 的时候,没能把它的 HDC 一并归还,这当然是我们软件中的代码错误,也是我们遇到的问题的直接原因。但故障现象的根本原因是什么呢?我们确实没有按照规范「一借一还」,至少窗口还「在世」的时候没有。不过 ReleaseDC() 难道不管三七二十一,只要有人拿着某个 HDC 来释放,它就答应吗?都不用看看 HWND 对不对得上吗?

微软 关于ReleaseDC()的API函数说明 在这一点上就有点语焉不详了。大概它没想到有人会这样去实践?的确也没人问这种问题,完全找不到资料,只好自己动手做了个实验:

HDC hDC = ::GetDC(hWnd);
int nRet = ::ReleaseDC(hWnd + 0x1000, hDC);

随后再在这个 DC 上用 GDI 函数画东西,确实画不出来。nRet 也的确是 1,按照微软的说法,返回值 1 表示 DC 被释放,这倒是没有骗人。我把 hDC 也加了个数字,然后再跑一遍,这次 nRet 变成 0 了。

所以说,微软是在 ReleaseDC() 的时候搞了个「容错」逻辑?只要 HDC 对得上,就给释放,不管 HWND 对不对得上号?我一开始跟 Google Gemini 探讨这个问题的时候,它还不相信,直到我告诉它测试结果。


回过头来看,为什么两次 GetDC() 能得到同一个 HDC 呢?
我们的主窗口,经历了销毁后重建的过程。在 OnDestroy() 的时候,没有及时执行 ReleaseDC()。但 Windows 可能认为,窗口不在了,DC 也就没了。于是另一个新建起来的窗口又通过 GetDC() 拿到了同一个 HDC。等到主窗口的 C++ 对象开始析构,调用 ReleaseDC() 的时候,新窗口拿到的 HDC 就被背刺了。系统的 DC 应该是在放一个池子里面,所以是有可能被重用的。这当然需要「运气」,也正因为如此,故障现象不是很稳定。

会遇到这种问题的人,应该不多。现在还在用 GDI 做开发的项目本来也就不多了。我在网上没能找到什么可以参考的信息,还好勉强算能够重现,就抓住机会解决了问题。经验值又 +1 了。

最后说明一下:我这个实验是用 VS2013 在 Win10 下面做的。不同的 OS 以及 VS 版本可能会有不一样的情况。毕竟是所谓的「未定义」行为。

2025-06-30

Win11 不能访问匿名 SMB 了?

要跟同事交换大量的数据,还涉及到手机。Samba 还是比较靠谱的选择。无论是 Android 还是 iPhone 都可以访问,用我一位手机开发组的同事的话来讲:「这比网盘好用多了」。

然而有一位同事说 SMB 目录打不开,过去一看,发现他电脑已经升级到 Win11 了,自己还浑然不觉,有点惊奇这种神经是怎么炼成的。

Win11 默认不准匿名 SMB 了,大概是出于某些「安全性」方面的考虑。但是我又不想去加上用户名/密码,于是琢磨了一下怎么把他的电脑给「降级」。这帮家伙 Windows 更新都停掉不装的,一个匿名 SMB 又怎么了?

组策略中改一改

先运行 gpedit.msc,然后「计算机配置」->「管理模板」->「网络」->「Lanman 工作站」,把「启用不安全的来宾登录」从「未配置」改为「启用」,就可以了。
有的人说还要改另外一处地方。反正我只改这一处就 OK 了。

2025-05-12

我的电脑史(二)——80386SX-33

最早接触 x86 的 PC,是在小学的电脑兴趣班里面。
校长看第一届电脑夏令营办得很成功,于是又追加了投资。CEC-I 的总数增加到了五六台,还有一台有机箱的电脑。显示屏、机箱、键盘各自分离,而且有内置的软驱,看起来很「高大上」,现在想来,应该是一台 IBM PC 兼容机。
之前负责夏令营的两位女老师,现在已经难堪重任了。取而代之的是学校里另一位老师的儿子,大概是大学生。有空的时候就来带带这个班,讲点「堆栈」之类我们都听不懂的东西。那台 PC 机,就成了他的专用电脑。

图片来自网络,非当时的照片

开机需要插入软盘,有一天我们趁他不在自己折腾进了 OS。印象比较深刻的是,软驱读盘像在弹吉他。还有就是「A>」的提示符是从屏幕下方「升」起来的,带着残影。
当时我还不会 DOS,连 Apple DOS 也没玩过。进了 OS,我们这帮小孩就不会了。大学生发现后倒也友善,看到我们对这个有兴趣,还表演了几个游戏给我们玩。TestDrive 我第一次就是在这里摸到的,还有打伞兵,以及机器人大战等等。

图片来自网络,这已经是彩显的 CGA 效果了,当时是绿显

这台机器因为我接触不多,再加上平时它一般都是用一块绸布给盖起来的,在记忆中一直比较神秘。
记得软驱只有一台,估计不是高密度盘,因为我记得载入 TestDrive 会提示换盘。这样看来应该也没有硬盘。
显示器是绿色的单色显示器,不过应该有灰度。显卡类型起码有 CGA,否则这些游戏应该跑不起来。声音就是靠机箱喇叭了,现在想想有点简陋,当时 TestDrive 那开场音乐还是叫得挺欢的。


初中的时候,爸妈从(另外一个)中学的一位电教老师那边,替我搞了一台组装机。

之前是我大伯先去搞了一台 PC/AT 兼容机自己放在家里玩,也是找这个老师。有两部 5.25 英寸高密度软驱,卧式机箱,不过 CPU 不是 80286。东西的来路不是很清楚,肯定不是品牌机。我很新鲜,也很眼馋,经常往他家里跑。爸妈看我这样,就问大伯电脑哪里来的,线就是这样牵上的。

后来想想,这不就是最早的垃圾佬兼二手电脑贩子嘛。不过当时并没有规范的电脑市场,所有的配件基本上要不就是单位采购,要不就是私下流转。对方的中学电脑课教师身份,应该是派了「大用场」。

我爸妈为了这台电脑大概花了三四千,可能更多。现在回想起来,也是一笔不小的数字,恐怕是一个人的年收入。后来我去找那位「卖」电脑给我们的老师,找他 hdcopy 一些软件的时候,他向我打听过我家的经济情况。当得知我爸妈从事汽车维修行业并且自己开店的时候,他有喃喃自语道「修车的有钱」。

我怀疑他是从这台电脑上狠赚了一笔,心中略微有愧,需要一些事情来让自己良心上过得去。别的不说,给我的那块 40MB 的硬盘,上面满是坏道。物理坏道,低级格式化也修不好的那种。换成现在的我,是下不去手赚这钱的。
还好坏道都集中在后 1 / 5 的位置,刚好 FAT16 最大也只支持 32MB 的分区,所以正好分成两个区,D 盘就扔在那边不去动它了。


无论如何,我有了自己的第一台真正意义上的个人电脑。

这台电脑的 CPU 是 80386 / SX,主频可以在 25MHz 和 33MHz 之间切换。前面板有个切换开关,还有一把小锁用来锁住机箱不让开启。

图片来自网络,非原图,只能说大致差不多

2MB 的板载内存,64KB 一片的集成电路(DIP 封装),半节 AAA 电池大小。在主板上铺了一大片,大家可以自己算算。

相比之下,我大伯的电脑只有 1MB 内存。因此我可以接触 EMS 和 XMS,能玩 HIMEM 和 EMM386(以及 DOS4GW),而他不能。当然他后来也去升级过了。

一台 5.25' 1.2MB 的软驱,再加一台 3.5' 1.44MB 的软驱,当时算是比较不错的配置了。
机箱比较窄小。40MB 的硬盘分了两个区,坏道都放在了第二个区。硬盘通过 IDE 线和一块「多功能卡」接入主板。嗯,现在大概没人知道「多功能卡」这个名词了。

显示器是一台「双频单显」,这个名词现在去 Google 的话得打引号才能看到靠谱的内容。灰色的,不是绿显。支持 Mono、CGA 和 HGA。最后这个大力神的分辨率一度给我带来了一些「惊喜」。当然,显示器得跟显卡配套,后来换彩显的时候都一起换下来了。


当时还没有 CD-ROM,所以数据唯一的入口只有靠软盘。我前前后后买了十几盒的 5.25' 1.2MB 软盘,以及几乎差不多数量的 3.5' 1.44MB 软盘。有一些是别的品牌,不过大部分都是 3M 的防霉盘,气味非常特别。
里面的软件,一些来自于那位老师,一些来自我大伯,还有一些是从《电脑报》报社搞到。最后还有一些是来自于邮寄目录,最有名的是《楚汉之争》。

图片来自网络,我买了起码一打

PC 上没有固化的中文系统了,需要外挂。现在的 UCDOS 一开始我并没有,不过有一张自带压缩字库的 WPS,很省内存和磁盘。后来搞到了 CCDOS,再后来有了天汇和中国龙。我还是很喜欢天汇这种小巧的中文系统,很容易就带走了。而中国龙则「贡献」出了自己的字库。

需要用到中文系统的时候不是太多,主要是用WPS写文章。我录入过大约半本书的《伦敦浩劫》,算是用来练习指法和双拼双音的输入。到后期愈发纯熟,输入速度越来越快,只可惜双拼现在已经差不多全忘了。
练习指法的 TT 更是常客,用的时候 PC 喇叭挺吵的。不过后来我到了大学也还是时不时练一练。很多同学也在用,大概老师有推荐,这是后话。

那个时候我掌握的一项「核心技术」,就是「腾挪」内存。用 HIMEM 把 640KB 常规内存给节省出来,印象中我最多能挤出 600 零几 K 的常规内存出来。这是从 MSDOS 5.0 开始的事情,没多久我就从 3.31 升级到了 5.0,也因此接触到了 QBASIC,后来大多也都是用的它。正经的 Quick Basic 我听说可以编译出 EXE,一直很向往,但到了很后面才用上,那个时候我已经不稀罕它了。

由于软件来源不足,到了后期我又开始了自己编程。因为有了 BASICA、GWBASIC,后来还有了 QBASIC。我开始接触到了结构化编程。相对于小学 / 初一时期,水平可以说又上了一个台阶,不过现在看来也还是在洼地里面扑腾而已。

有点后悔当时没能学学 C。Turbo C 我那个时候是有的,但是一听到「C语言」大家都觉得是大学里面才学的东西。而且听说是两代半的语言,于是我也怕难不敢去碰。
后来那位老师有一次「介绍」我去帮他一位同事(或朋友?)打过半天的工,帮他写代码,用 GWBASIC 写。大概是一个教学用途的工控项目,用 BASIC 写也不是不行,有点勉强。那位大叔大概懂硬件,向串口发数据之类的东西他写,UI 就让我来写。写了一个下午,给了我 10 元算辛苦费吧。我也不知道这算啥?我是被卖了吗?


这台电脑后来经历了一些硬件上的升级。回想起来大概是 94 - 95 年。

换了彩色显示器,直接上了一块 Trident TVGA 9000 卡,显存有 1MB。我后来找遍资料都只找到 512KB 显存的 TVGA 9000,所以很疑惑这个型号是否正确。但用起来的确没有问题,BIOS 里面也是那样写的。问了 AI 说有,那就有吧。

硬盘也去换了一块 420MB 的硬盘。40MB 那块的坏道实在是多,容量也有限。这两样加起来又是两三千块。回想起来,那些年我父母的确算是「赚得动」。

图片来自网络,这块硬盘后来换掉了

换硬盘的时候有一段小插曲:为了砍价,我最后提出来,要用硬盘把他们那边的「正版」软件给 copy 一些带走。胃口太大,选了一大堆,硬盘放不下,最后还是用了一些软盘。


有了更大的硬盘之后,我开始接触一些更高档的软件了。中文系统不再局限于压缩字库的 WPS(话说我觉得那个压缩字库还挺好看的)。也用上了 Windows 3.1,以及稍后的中文版 3.2。随后也接触到了 Microsoft Word 6.0 for Windows,以及 Visual Basic 6.0。

还记得当时安装 Windows 3.1 要用 6 张软盘,后来也有 5 张盘的版本。我最后一次去找那位老师用 hdcopy 复制 Windows 3.1 的时候,撞上房间里面有另外一位年轻的女性,鬓发有点散乱。老师言语间颇有些惋惜和责怪的意味。我当时确是有点不识趣,后来才回过味来,此后我就没再去找过他。

我有的时候还挺怀念单色显示器,后来即使用上了彩显,也偶尔把它换回来怀一下旧。其实在双频单显的时候,我就已经用上 Windows 了。在 HGA 模式下,Windows 虽然只有单色,但分辨率其实算是够用了。「切换显示模式」这个术语对于现在的 PC 使用者应该已经相当陌生了吧?一些台湾出品的 CAI 软体很喜欢用 HGA 这个显示模式,我也因此学习了不少繁体字和台湾的 IT 用语,以及 IT 知识。

再后来,它就有点缺乏升级潜力了。内存不足是最大的问题,虽然有 SIP 的扩展口,但国内那种内存几乎找不到。去升级的时候,电脑商家也表示这种内存已经过时,不建议我继续在上面投入。

CPU 也是焊死在主板上的,拔不下来也换不掉。主频比较低,假 32 位,还没有 FPU。如果要去搞一块 80387 加上去,也觉得是浪费。我大伯倒是后来把他那台电脑升级成了 80386DX + 80387。

本来我这台电脑就可以算作以过时淘汰的电脑配件组装起来的,所以也没有必要硬为它续命了。上高中前我换「多媒体」电脑的时候,就把它的主要配件一次性都换掉了,那就算是另外新买了台电脑了。所以放在下个故事里面继续说它。

2024-07-27

「微软蓝屏事件」与「信创」

图片来自网络,与本文内容无关

「微软蓝屏事件」也已经过去一个多星期了。各方信息差不多到位了,讨论得也相对充分,应该不需要我再来分析或者补充什么了。

不过,这个事件发生之后,我遇到一种有意思的想法,可以与大家分享一下。

蓝屏幕席卷全球的时候,我正在旅游。连日记都只能在手机上简单记录一下要点的我,完美错过了这个事件的波峰。等我有空来了解的时候,整个事件基本已经有了结论,甚至都快平息了。我不知道这算我的幸运还是不幸。

当然,余波还是在的。等我周一回到公司,有同事就向我表示,「信创」的事情,可能要因此而加速。理由是这次中国在此次全球性事件中基本幸存,有赖于「脱钩」,那么接下来这个过程应该会更加地紧锣密鼓。

这自然是他自己的理解,不过也很有可能会是一个「正确」的预测。我们都知道所谓的「上头」其实也只是一届「草台班子」而已(实际上谁不是呢?)。他们的见识不见得比普通人高出多少。所以,按照常识,他们很可能也会这样去想,去思考,也就会真的这样去做。

问题是:中国这次的「幸存」,真的是因为「脱钩」吗?

跟许多人的理解可能不太一样,这次微软所谓的蓝屏事件,实际上跟微软没有什么关系。在我看来,这事就是一个装机量巨大的杀毒软件出了一个 Bug。由于在企业用户中装机量巨大,企业服务的客户也被影响到,于是闹出了这么大一个事端。微软还真是可怜,其实是它被别的软件给「搞」了。

可能现在活跃在网上的人都太年轻,也或者互联网真的是没记忆了。但我可是都还记得的。
我曾经写过一篇 Blog《突发的 c000021a 蓝屏故障》。2007 年,距今有一些年头,15 年有,但还不到 20 年。在我看来,那次的事件比起这一次,其实还更为严重一些。这次只是内核驱动层面出了问题而已。驱动嘛,开个安全模式就进去了。然后把文件夹改个名,让它正常启动时加载不了,就恢复了。2007 年那次可是把系统 DLL 文件直接给干掉了,相当于把操作系统删掉了一部分,安全模式都没用。单机只有哭。

我并不是太清楚现今的 CrowdStrike 与当年的 Symantec 哪个更牛逼。反正我现在不用什么杀毒软件或安全防护软件。「自己的安全自己负责」,很多年前我就学会了自己解决问题。普通人肯定没法照搬我的情况,这也是为什么 360 这种东西至今也还能大行其道的原因。
那么,说到这里,我的另一个大问题就要抛出来了:你为什么就能确定,360 之流不会闹出同样的问题呢?

在我看来,作为一个拿到了 ring0 权限的软件,要把系统搞蓝屏,那简直是举手之劳。能把你 Windows 搞蓝屏的内核驱动,我要不了几分钟就能写上一个。像 CrowdStrike 那样的情况,我们一般称之为「逻辑炸弹」,也不是什么多难的事情。无怪有很多人对此事归结于阴谋论。
我觉得这种逻辑炸弹恐怕还有很多,最好不要被什么歹人给拿到。但对此你无法打包票,所以还得要有自己的能力,以及要有备援才行。我上面说的那句话的扩大版,此时应该强调一下:自己的事情,只有自己来操心才靠谱,靠别人都是靠不住的。

况且,国内的软件企业就很靠谱吗?
这次很多人觉得很不可思议的一点是:CrowdStrike 既不好好测试,也不做灰度更新,直接全球推送,非常大胆,非常的不专业。有人发现,CrowdStrike 的创始人之一,之前在 McAfee 也闹出过类似的问题。这一点还真是,我也不知道如何吐槽。
CrowdStrike 是草台班子,这一点可能没错。但国内的软件企业,我可是更加不放心。

我曾经有个同事,脑子算是聪明。别人上大学的时候,他当兵去了。复员后也没受过什么系统性的专业训练,「怎么写代码」这件事情,都是自己琢磨的。其实从这一点就能看出来,人的确是挺聪明的。但他路子也挺野。若让他去研究技术问题,我认为或许会是一把好手,但不应该去做工程师。他写出来的代码,Bug 不少。
他有一种自创的加密文件格式,其中的加密算法基本上就是异或,关键是密钥是他自己的硬编码。这个东西用在了他当时给公司开发的产品中。后来他离职了,代码就交接给了我。「Bug 多」的印象,也就是这个时候留下的。
听别人说他去了某大城市发展。再后来,可能四五年后了,我在研究 360 的一款产品的时候,发现它们的漏洞库文件的 HEX 密文看上去很眼熟。于是我用那位同事留下的代码去试了一下,没想到真的一下子就解密成功了。
这……

所以,「脱钩」并不能保证不受影响。相反,闭门造车,低水平造轮子,还加上不用接受市场检验,由政府背书和「保送」,使得出现这种问题的机会,其实是增加了。
要问什么才是真正有效的手段,窃以为,增加多样性,增加市场竞争,算是一个。
所谓「信创」,不管背后的内容是什么,重点应该放在「创」字上。如果还是现在这种搞法,把重点放在「不被卡脖子」上,不可能解决这个问题,反而会恶化。

如果能创造出更多样的生态,有了更多的选择,就不会吊死在一棵树上。最理想的情况,每个软件都是不一样的,就不会因为一个弱点而全球瘫痪。
就像生物世界一样,每个人的 DNA 都是不一样的,没有什么病毒可以杀死 100% 的人类,总有人能幸存下来,然后把抗病毒的基因传播下去。

说到底,科学技术范畴的事情,就要遵循科学技术的发展规律,而不要想着去为自己的政权安全服务。出发点如果就「心术不正」,最后是结不出什么好果子的。

2017-05-21

正确地获取 Windows 的版本号

以前,想要获取 Windows 的版本号很简单,有个 Win32 API 函数名字叫做 GetVersion,望文生义,接下来要做的事情就是去 MSDN 上查下用法就可以了。

现在,GetVersion 会被报告成「过期函数」了。也许还能用,但(据 MSDN 说)起码在 Win10 上是别指望得到预期的结果了。道理也很简单,Win10 都搞滚动升级了,版本号规则肯定也和之前不一样了,你还指望这么老的函数能兼容么?

别痴心妄想了,GetVersionEx 也一样过期。那么,眼下有什么好办法吗?

一般来说,拿 Windows 版本号可能有两种用途:

  • 我想看看你 Windows 版本达到我要求没。
  • 我就是想知道你 Windows 版本号是多少。

对于前者,微软现在在 MSDN 上是这样推荐的:它做了一组 Version Helper functions,你如果想知道当前 Windows 的版本是不是某个特定的发行版,调这组函数就可以。我们来看看这组函数中三个典型:

  • IsWindowsXPOrGreater
  • IsWindowsXPSP3OrGreater
  • IsWindowsServer

不需要更多说明,我们从名字中就可以看出,这组函数可以用于判断 Windows 的大版本,Service Pack 的版本(结合大版本),以及能知道是不是服务器版操作系统。通常情况下,这些函数大概是够了。

但是,有的时候我们并不关心版本号高低,我们只是想要一个版本号(例如记录日志时)而已。微软对此的建议是:用 GetFileVersionInfo 去获取一个系统 DLL(例如 Kernel32.dll)的文件版本号(原文看 这里)。

相关的代码虽然能找到,MSDN 上也有官方例子(有点小 Bug),但比起一行 GetVersion 来代码量实在是不能算很少。由此可见,处理「过期函数」真的没有想象中那么容易。最后我还是提供一下我从项目代码中挖出来的一个实现吧。别照抄,如果你不想引入 STL 的话:

#include <windows.h>
#include <Strsafe.h>

#pragma comment(lib, "Version.lib")

// 获取文件版本
std::wstring GetFileVersionString(const std::wstring& strFilePath, bool bStrVer = false) {
    DWORD dwVerInfoSize = GetFileVersionInfoSize(strFilePath.c_str(), nullptr);
    if (dwVerInfoSize) {
        std::vector<BYTE> vecVerData(dwVerInfoSize);
        if (GetFileVersionInfo(strFilePath.c_str(), NULL, dwVerInfoSize, &vecVerData[0])) {
            LPCVOID pBlock = &vecVerData[0];

            UINT cbTranslate;
            TCHAR SubBlock[MAX_PATH];
            struct LANGANDCODEPAGE {
                WORD wLanguage;
                WORD wCodePage;
            } *lpTranslate;

            // 阅读语言和代码页列表
            VerQueryValue(pBlock,
                L"\\VarFileInfo\\Translation",
                (LPVOID*)&lpTranslate,
                &cbTranslate);

            if (bStrVer && lpTranslate) {
                // 读取第一种语言和代码页的文件版本
                for (size_t i = 0; i < (cbTranslate / sizeof(struct LANGANDCODEPAGE)); ++i) {
                    StringCchPrintf(SubBlock, sizeof(SubBlock) / sizeof(TCHAR),
                        L"\\StringFileInfo\\%04x%04x\\ProductVersion",
                        lpTranslate[i].wLanguage,
                        lpTranslate[i].wCodePage);

                    LPVOID lpBuffer = nullptr;
                    UINT dwBytes;
                    if (VerQueryValue(pBlock, SubBlock, &lpBuffer, &dwBytes) && lpBuffer && dwBytes > 0) {
                        std::wstring strVersion(reinterpret_cast<TCHAR*>(lpBuffer));
                        return strVersion;
                    }
                }
            }

            // 未找到任何字符串版本
            VS_FIXEDFILEINFO* lpffi = nullptr;
            UINT uLen = 0;
            // 注意:这里的第二个参数 "\" 是固定写法,表示查询根块
            if (VerQueryValue(pBlock, L"\\", (LPVOID*)&lpffi, &uLen) && lpffi && uLen >= sizeof(VS_FIXEDFILEINFO)) {
                std::wstringstream wos;
                wos << HIWORD(lpffi->dwFileVersionMS) << L"." << LOWORD(lpffi->dwFileVersionMS) << L"."
                    << HIWORD(lpffi->dwFileVersionLS) << L"." << LOWORD(lpffi->dwFileVersionLS);
                return wos.str();
            }
        }
    }
    return L"";
}

// 获取 OS 版本信息
std::wstring GetOSVersion(const std::wstring& strWinSysDir, bool bStrVer) {
    std::wstring strWinSysFilePath = strWinSysDir;
    if (!strWinSysFilePath.empty() && strWinSysFilePath.back() != L'\\') {
        strWinSysFilePath += L'\\';
    }
    return GetFileVersionString(strWinSysFilePath + L"Kernel32.dll", bStrVer);
}

2017-05-18

当 Win7 Windows Update 遭遇 0x80073712

Windows Update 一直以来都以会遇到各种 Error 代码而闻名。今天又遇到一例,记录一下。

起因是 WannaCry。我有一堆各种 OS 版本的虚拟机,其中一台 Windows7 SP1 x86 使用得很不频繁,昨天打开一看,上次 Windows Update 已经是 2016 年 09 月的事情了。虽然 NAT 挡在宿主机后面其实不会有啥问题,但是按照我的习惯,下班前还是让它去打了补丁。曾经在上一家公司的遭遇一直在提醒我:有人的虚拟机中了震荡波,然后不知情的时候被做了快照,于是每隔一段时间测试机房就会忙活一阵子(测试机为了测试程序的补丁管理功能是不打补丁的)。

今天早上一来,红色儿的,4 个成功 2 个失败。我也没太放在心上,公司网络有时候会断,说不定是下载失败。再来了一次,在下载到 11% 的时候又失败了。我把 VPN 开起来(曾经有不开 VPN 打补丁会下载失败的经历),上了个厕所回来,然而这次还是失败,我看了下 ErrorCode:80073712。每次都是这个。好吧,开始 Google

官方网页 推荐的做法大概是这样的:对于 Win7 而言,首先请先尝试用 SFC 修复一下。如果还不行,那么请下载工具 System Update Readiness tool 进行修复。

SFC 这货其实没啥鸟用,反正我每次用都没啥好结果。这次也不例外,扫描到 44% 时告诉我:虽然我们发现有错,但是无法修复,你去看日志吧。

试了下再次 Windows Update,还是报 0x80073712。好吧,只好试试看那个修复工具了。下载下来两百多 MB,安装了老半天。再次 Windows Update,这回进度开始超过 11% 了,我长舒一口气。终于 OK 了。

顺便瞄了一眼同页面上对 WinXP 的问题处理建议,仅仅提到 SFC。看来真的是该放弃这破烂了。

2015-03-11

在 Windows 上用 x64 的 Python 运行 GoAgent

首先声明:本文适用于至少懂编程,并且爱折腾的人。不一定要很熟悉 Python,但至少应该入过门了。

GoAgent 自带 Python27 运行环境,不过因为本机上也安装了 Python 2.7 x64,所以希望能直接使用我安装的 64 位版本。总结下来,要点有:
  • 无论你是否准备使用 x64 版 Python。GoAgent 与 Python 2.7.9 不兼容,应该安装 Python 2.7.8 版本。
  • 要运行 GoAgent 还需要 gevent 和 pyOpenSSL。可以手动安装,也可以通过 pip 来安装。推荐后者因为省事。
  • Python 2.7.8(无论是否 x64)及更早的版本,没有自带 pip。所以要去下载 get-pip.py,然后用 python 运行这个脚本安装 pip。
    ps: 用 get-pip.py 下载到的 pip.exe 的版本,比 Python 2.7.9 里面带的要新一些。所以就算有自带我也会换掉它。

另一个重点来了:
有些版本的 GoAgent 里面对于 crypt32.dll 的加载代码在 x64 的 Python 中有问题,需要修改。改成像这样就可以了:
crypt32 = ctypes.WinDLL(u'crypt32.dll')
crypt32_handle = crypt32._handle
释放的时候要这样:
ctypes.windll.kernel32.FreeLibrary.argtypes = [HMODULE]
ctypes.windll.kernel32.FreeLibrary(crypt32_handle)
在前面还要:
from ctypes.wintypes import *
否则 x64 下会报错,因为 HMODULE 和 long 的长度在 x64 下不一样了。

我用的 2.1.17 的 GoAgent 中,对于 crypt32.dll 的加载代码还位于 proxy.py 中。最新的 GoAgent 已经移到 proxylib.py 里面了。什么时候移的我没有去考察过。代码改法可能略有不同,不过思路应该是一样的。

折腾好以后。把本机上的 Python.exe 复制一份改名成 Python27.exe。接下来就可以把 GoAgent 自带的 Python27.* 给删掉啦。

2014-10-31

SingleThread 下遇到的并发问题

手下某个小弟,有一天报告我说他写的某个 Win32 Application 有一个奇怪的 Bug,搞了半天搞不定,向我寻求支援。Bug 现象是:下载文件,完毕弹框提示,点掉之后报错,Crash。

通常而言,这种问题,往往是因为在释放、删除什么东西的时候,该做的事情没做对,比如对着一个对象的指针进行了重复 delete 之类。但看了下代码,没觉得这方面有什么问题。因为这是个 SingleThread 的程序,于是尝试用单步跟踪跟了一下,发现有一段代码似乎在所属对象析构之后还在跑。这就有点奇怪了:SingleThread 的 Application,不应该有这种属于 MultiThread 的毛病才对。Socket 模型用的是 AsyncSelect,也就是说「异步」是用 Windows 消息做出来的,并不是真的「并发」。那么到底是哪里不对劲呢?

再接下来分析发现,虽然是 SingleThread,但最后出错前弹的那个提示框,是在 OnReceive 的时候通过 SendMessage 去弹的。这样就有眉目了:ModalDialog 并不阻塞 ParentWindow 的消息循环,所以在弹框等待用户确认的时候,消息循环收到了 OnClose,于是 Socket 对象在用户点击确认按钮之前,其实已经被 Destroy 了。之前还没跑完的 OnReceive,接着再跑的话,当然只能 Crash 了。

分析到这里,问题就已经很明白了:这就跟 MultiThread 下临界区没加锁一样嘛。你以为 SingleThread 下每个函数就都是原子操作,不会被乱入的东西打搅?呵呵,你一 DoModal 就会给你再嵌个消息循环进去的。可怜很多小弟连 DoModel 的原理都没搞懂就开始写程序了。我上次还听几个小弟在争论相关问题呢。不是说写程序必须啥都弄明白才能开始,但若是只拎半壶水就开跑,将来就难免会碰上这种「奇怪」的问题。

要修正这个问题,也很简单,改成用 PostMessage 让 MainWindow 自己去处理弹框的事情就可以了。不过有点奇怪的是,在 XP 下好像不会看到错误现象。Win7 下直接运行 EXE 也不报错。只有通过两层以上的 CreateProcess 去调用,才会看到现象。难怪没什么用户报告这个问题。是不是 OS 觉得这个 EXE 反正会很快地 Over 掉,有些错误就不报算了?看来微软在私底下还是有一些没告诉大家的小动作的哈哈。

总结一下,这个案例教育我们:
  1. 不要以为只要是 SingleThread 就一定不会遇到并发问题。
  2. 前/后台逻辑应该要区分清晰,是后台代码就别抢前台的活儿。
  3. 还有,SendMessage / PostMessage 不要不经大脑就乱用。

2014-07-31

Alipaybsm.exe 是个有意思的东西

在接下来的篇幅中,我要讲一个目前还没结束的故事。故事可能还会继续发展下去,也可能因为我的懒而就此打住。但至少我觉得目前已经有足够有意思的信息可以让诸位知道了。这件事,跟支付宝有关,跟(网络)信息安全也可能有一些关系。有兴趣的朋友,可以接着看下去。


我以前曾写过一个服务器 Ping 值测试程序(参见这里《写了个批量测试服务器Ping值的小工具》)。这个程序一直都能满足我的需要,直到有一天在我太太的笔记本 Win7 x64 系统上遇到了问题:对几乎所有的 IP,我这个程序的 Ping 都很快收到了回应,快得不正常,几乎就像做了个本地调用一样,与实际情况不相符。于是我打算看看这是怎么回事情。

当时我人在公司,VC6 远程调试又不方便。最后靠着 DbgView 终于搞清楚了:接收到的数据中,多出来了一份不正常的东西。我之前的代码,并没有估计到这份不正常的数据可能会出现,所以处理上出了些问题。

OK,这算是我的 Bug。可这「不正常的数据」到底是什么东西?我把它 Dump 出来一看,还真是有点奇怪!ICMP Type 是 8,源地址和目的地址则与预期的 Echo 回应包刚好相反。算上 sendto 时候系统自己加上的 IP 包的包头,跟我送去发送缓冲区里的数据那是一模一样。

要解决我程序里的这个问题非常简单。但是另一个问题就不那么好回答了:为什么其它电脑上不会这样,偏偏这台电脑会出现这种奇怪的事情?
直接答案很简单——它一定跟别的电脑有什么地方不一样!
那么还有第二个问题:到底是什么地方不一样呢?

可以说是我的幸运,也可以说是阿里集团的不幸。因为我的 Taskmgr 里面进程列表设置为按 ASCII 字母序排升序的缘故,我很快就找到了这第二个问题的答案:Alipaybsm.exe。杀掉Alipaybsm.exe 这个进程,前面提到的那份「不正常的数据」就不再出现。而这个 Alipaybsm.exe 似乎由 AlipaySecSvc.exe 在守护,过了一会儿就又自己启动起来了。它一出现在进程列表中,我一试,哈,那个奇怪的现象就又出现了。

后来,我把这事情在 Twitter 上说了一下,还引发了一场小小的讨论。
我目前还没完全想明白 Alipaybsm.exe 这样做的目的是什么。初步感觉,有可能是跟背地里监控网络流量有关。毕竟,目的地址不正确的数据,就算被放入 Socket 接收缓冲里面,在网络层与传输层之间估计也被滤掉了。我这次是因为用了 SOCK_RAW,需要自己下到网络(IP)层来处理数据,才碰巧发现了这个情况。如果只是在传输层(TCP / UDP)从事工作,估计不会有任何察觉。
只不过,反过来讲,如果能做到复制数据到 Socket 接收缓冲,那应该完全可以做到监控流量而不带任何痕迹才对。所以我目前还只能理解为,Alipaybsm.exe 想完全监控网络流量,所以利用了这个手段(复制发送的数据到接收缓冲中),但干这事屁股没擦干净(也可能没法擦干净),才产生了我遇到的这些情况。

我本来以为当时那个 Alipaybsm.exe 是个假货。但看 EXE 的详细信息,以及绑定的数字证书,都像是支付宝官方的真货。我又以为那只是一个不成熟的版本,可能有 Bug,但我前两天为了转一笔账,又去下载并安装了一个支付宝安全控件,然后它又出现了,带着它那奇怪的行为又出现了。
所以,我们来仔细看看这货吧:

看上去挺正常吧?

在 Twitter 上讨论的时候,有人表示,在 Mac 上用防火墙没观察到有这个现象。为此,我今天特意去确认了一下:在 Windows 上抓包,也观察不到这个现象。我估计,只有自己写基于 SOCK_RAW 的程序,才能收到这些数据。为了检查这种特殊的行为,我专门写了个小程序 AlipaybsmTester,基本上就是一个单地址单次单线程的 PingTester。

从这幅截图中可以看到,Microsoft Network Monitor 只抓到了一来一回共两个包,但我的测试程序发了一个包收到了两个,内容各不相同。如果杀掉 Alipaybsm.exe,那就只会收到后一个包了。

接下来再看看这个 Alipaybsm.exe 的一些更好玩的事情:
很奇怪的是,它其实并不是随着「支付宝安全控件」(Aliedit.exe)装上去的。当你登录支付宝,根据 Web 页面上的提示安装了「支付宝安全控件」时,只会在 Program Files (x86)\alipay 下面建一个名字叫 alieditplus 的目录。

但是过一会儿(我这次过了 30 分钟左右),在 alieditplus 下面会出现一个 update 目录,并下载一个 SafeTransaction_Setup.exe 放在其 \job\file\tmp\zip_1009_ 子目录中(不同时期不同环境中路径可能会有所不同)。随后 Program Files (x86)\alipay\SafeTransaction 目录便出现,里面就有 Alipaybsm.exe(当然还有一些别的)。

我在网上想搜一下关于这个 Alipaybsm.exe 或 SafeTransaction_Setup.exe 的相关信息,发现少得可怜。有一篇 「百度知道」的问题 在问为什么 Alipaybsm.exe 可以提升网速。我估计提问者是从迅雷或 360 流量监控浮窗上观察到了这种现象吧?其实这就是 Alipaybsm.exe 在偷偷复制数据包到接收缓冲中的结果。那些在接收缓冲中突然多出来的数据,在第三方看来就是网速翻倍了。

Alipay 官方则完全没有提到过这些东西,好像它们是感染了 AIDS 的私生子一样。不过每个安装了支付宝安全控件的电脑上,估计都会有这些个东西(还有个 AlipayDHC 也值得注意)。我认为以这种方式进行推广的程序,很可能另有其目的,不见得真的是保障个浏览器安全这么简单。如果真是为了保障浏览器安全,完全可以公开(乃至大张旗鼓地)宣传,然后打包到安装包里一起分发下去正大光明地安装,不是吗?

PS: 我后来发现,杀掉 AlipaySecSvc.exe 也会导致复制数据包的现象中断,并且重启该服务之后,恢复现象花的时间比单单杀掉 Alipaybsm.exe 要长。可见 Alipaybsm.exe 的角色大概只是一个行动的发起者和结果的分析者,具体对流量实施监控的行为,很可能是它去调用 AlipaySecSvc.exe 中的某些个服务来完成的。这说明对于「支付宝安全控件」本身也不能掉以轻心。相关功能其实可能一直就放在 AlipaySecSvc.exe 中,只是没有人来扣扳机而已。而这个扣扳机的可以是 Alipaybsm.exe,也可以是别的谁,那谁谁谁。

2008-04-29

在 Windows 服务中以 SYSDBA 无需密码登录本地 Oracle

因为客户的特殊需求,导致我们要在一个 Windows 服务中以 SYSDBA 权限去访问本地的 Oracle 数据库,并且不能输入任何密码。通常情况下,当 Windows 的用户帐号是 ORA_DBA 组成员时,直接用 sqlplus "/ as sysdba" 的方式就可以登录本地 Oracle 数据库,无需输入 sys 的用户名和密码。因此我计划用这种方式来实现无密码登录 Oracle。但是得到了如下的错误信息:
ORA-01031: insufficient privileges
很明显,权限方面出了问题。想想也应该,我们的 Windows 服务使用的应该是 NT AUTHORITY/SYSTEM(S-1-5-18) 的权限在运行。于是方案 A 出台了。

方案 A:将 SYSTEM 帐户加入 ORA_DBA 组。

从 UI 是看不到 SYSTEM 帐户的,只有用命令行的方式加。
net localgroup ORA_DBA system /add
结果还是得到了 ORA-01031。分析了一下,有可能是因为操作系统认证需要从操作系统的用户帐号来进行,而 SYSTEM 帐号是个内置帐号,在用户管理里面是看不到的。于是这个方案宣告失败。

方案 B:切换到别的帐户运行。
基本上,方案 A 走不通的话,那就只有这种办法了。首先是建立一个 Windows 帐户,并加入 ORA_DBA 组中。密码自己随便定一个就好。
net user dbauser 1qaz2wsx /add
net localgroup ORA_DBA dbauser /add
但是具体怎么切换呢?

方案 B.1:用 runas 切换帐户。
一般 RunAs 服务(也就是 Secondary Logon)都是启动的,所以首先想到用 Windows 2000 / XP / 2003 自带的命令 runas 来实现。
runas /user:dbauser "sqlplus \"/ as sysdba\" ……"
没报错,可是没反应。噢,麻烦来了。这个 runas 命令是要问密码的。
上 Google 找了一下解决方案。试了一下用 vbs 的 SendKeys 那种,手动状态下似乎勉强可行,但要放在 Windows 服务中,没戏。因为它毕竟是模拟客户端的输入来键入密码的。有个自称可以让 runas 支持管道输入密码的小工具 sanur 也没戏,感觉它俩的原理是类似的。

方案 B.2:用第三方工具替代 runas 切换帐户。
既然 runas 这条路走不通,就彻底抛开它另寻出路了。
有个工具 lsrunas 自称可以替代 runas,于是下载下来一试。虽然参数比较罗嗦,但的确是可以替代。不过这个替代也仅限于通常情况下,一旦放到我们的 Windows 服务中,就没有反应了。
另外有人推荐了一个 cpau。这个工具还是不行,不过它倒是留下了一个提示信息,这个信息成了重要的线索:
ERROR: CPAU doesn't support running from LocalSystem.
看来还是 SYSTEM 的权限问题。

方案 B.3:自写工具切换帐户。
看来这些工具都没问题,但是应该都不支持在 SYSTEM 的权限下面切换用户。听说以前某个同事也为这个问题研究了好些天,最后还是放弃了。老实说,听到这个消息我有一点点绝望感。
这 SYSTEM 权限吧,说它低,却能操作很多系统级别的东西。可要说它高呢,有些本来很容易的事情它又做不了。不过理论上通常还是认为 SYSTEM 的权限比较「高」,所以很多黑客都以获得 SYSTEM 权限为目标在半肉鸡上奋斗着。于是,我以 SYSTEM 降权为主要话题,去黑客站点看了看。
还真是有收获,得到了一段代码,是一个利用 API 自己实现的 runas 工具程序。在命令行下使用了一下,感觉挺好,比 Windows 那个 runas 好用多了。
不过,放进 Windows 服务里面,还是不行。真的是快绝望了。

方案 B.4:用 API 直接切换帐户。
由于我们的系统有现成的接口用来启动一个 PipeCmd,所以到目前为止我都是用它去运行了一个批处理,然后在批处理中去做上述操作。该接口其实是用 CreateProcess 实现,因此新建的进程就继承了父进程的权限,也就是 SYSTEM。
但,在那段黑客代码中,切换帐户其实是使用了 CreateProcessWithLogonW 来完成。估计各个工具都类似,因此应该可以跳过中间的多余步骤,用 API 切换帐户来直接运行 sqlplus。
经过多次实验,总算是成功了:
HANDLE hToken;
LogonUser("dbauser", NULL, "1qaz2wsx", LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken);
STARTUPINFO si;
si.cb = sizeof(STARTUPINFO);
GetStartupInfo(&si);
PROCESS_INFORMATION pi;
CreateProcessAsUser(hToken, NULL, cmdbuf, &sa, NULL, TRUE, NULL, NULL, NULL, &si, &pi);
想偷懒的人还是看看代码稍微理解一下吧,因为我也想偷一下懒,直接 Copy & Paste 可能会有问题的。
另外说明一下,CreateProcessWithLogonW 应该也是可以做到的,但是我一直没能试成功。倒是 CreateProcessAsUser 一次成功,于是就这样用下去了。

本来在这里就应该作结了,不过后面还有一点小插曲:这段代码在 Windows 2000 / XP 上可以运行得很好,但是在 2003 上有问题。在 2003 上用这种方式运行 sqlplus 的时候看起来一切正常,但 sql 的内容没有被执行。最终的解决方案是把 dbauser 也给加到了 Administrators 组里面。
net localgroup Administrators dbauser /add
但是在 XP 下面要这样做的话,就有可能会让 Windows 把 dbauser 当成了主管理员,特别是在某些装好之后默认用 Administrator 用户登录的系统上。这样会给用户带来不小的困扰哦,所以最佳的做法应该是判断一下操作系统的版本再去做相应的处理。

2006-08-14

台式机的硬盘问题解决了

 硬盘没有毛病,其实是因为对 137GB 的大容量硬盘支持不够好所致。一共有两个可能的故障点:

  1. 主板 BIOS 要支持超过 137GB 的大容量硬盘。如果不支持的话,那么这种硬盘将无法加载,因此也就无法分区。我的主板不存在这个问题,不过我还是趁此机会把 BIOS 刷到了最新。
  2. 操作系统要支持超过 137GB 的大容量硬盘。如果不支持的话,就会出现和我前一段时间所遇到的情况类似的故障:分区莫名其妙丢失,数据(文件)随机损坏,故障由硬盘的某种读写操作引发,带有一定随机性,却检查不到任何物理坏道。

98 是一定会出问题的。2000 和 XP 需要打补丁解决,不过 2000 SP4 和 XP SP1 都包含了此补丁。然而,只打补丁还不够,还得把一个注册表项打开(添加上),才能使系统对大容量硬盘的支持生效。

该注册表项位于 HKLM\SYSTEM\CurrentControlSet\Services\atapi\Parameters 下,名叫 EnableBigLba。如果没有此项,那么需要手动添加,类型用「双字节值」,并把值设为 1。

其实超级兔子可以代劳这个工作。我记得很早的时候它就有了一个「打开 48 位 LBA 支持大容量硬盘」的选项。然而我一直不知道什么算是「大容量硬盘」,因此也就没有太在意这个选项。这次我把它选上之后,仅仅是重启,我丢失的分区就重新钻出来了,里面的内容全部都没有损坏。

我又观察了近一个星期。按照以前的教训,只要是开始用 BT,不出一天,一定就会出问题。现在回想起来,我存放 BT 下载文件的那个目录大概就位于 137GB 的界限附近。根据微软的说法,在此附近进行硬盘写操作,在未打开 48 位 LBA 的机器上就会出现数据的溢出。至于这个溢出会造成什么样的后果,就只有天知道了。有可能什么事也没有,也有可能会坏掉很多数据。

到今天为止,一切正常,看来故障应该是被正确检测到且修正了。