公司动态

Windows客户端开发进阶:安全厂商技术栈、系统编程与实战解析

📅 2026/8/28 10:09:38
Windows客户端开发进阶:安全厂商技术栈、系统编程与实战解析
1. 岗位分析与能力模型Windows客户端开发在安全厂商的定位2020年那会儿我在武汉看机会客户端开发方向的岗位其实不少但奇安信这个岗位吸引我的点在于它不是在招聘一个普通的CRUD程序员而是在做安全终端相关的客户端研发。我先把这类岗位的实际定位和底层逻辑拆开讲这样你投简历或准备面试时脑子里会有一张完整的能力地图。先说客户端开发工程师在安全公司的真实位置。安全厂商的终端产品本质上是一个常年驻留在用户电脑上的守护进程。它不同于普通业务软件对运行的稳定性、资源占用、兼容性有极高要求。奇安信这样体量的公司终端安全产品线的Windows客户端承载的是文件扫描、网络拦截、行为监控、漏洞修复等一系列重活。这就决定了岗位的技术栈不会是“会用某个框架就行”而是要对Windows系统机制有深入理解尤其是底层API、进程线程、注册表、服务、文件系统过滤这几个板块。再说为什么这个岗位放在武汉。2020年前后国内安全厂商和一线互联网公司都在二线城市铺研发中心武汉高校多人才供给充足人力成本相对可控同时又能贴近教育、政企市场的本地化需求。奇安信在武汉的研发团队基本都是承接总部产品的模块研发和定制化交付不是边缘业务。这一点很重要——你在武汉做的Windows客户端开发和在北京总部做的技术含量和项目体量没有本质差别。把岗位拆开看能力模型大概分三层第一层是语言和基础。这个岗位默认你会C而且是现代C11/14/17标准因为安全客户端对性能敏感历史代码也大量用C维护。另外要熟悉STL、智能指针、多线程编程这些属于基本功面试时一定会被深挖底层实现和内存模型。第二层是Windows系统编程。这层是Windows客户端开发和普通后端开发最大的区别。你至少要熟悉Win32 API、消息循环、窗口机制懂DLL的编写和注入原理了解Windows服务程序的编写和部署能处理注册表、WMI、事件日志这些系统组件。如果做终端安全还需要了解文件系统迷你过滤器minifilter、网络分层服务LSP/WFP、内核回调这些偏底层的机制。这些不是“加分项”而是“必备项”。第三层是工程化能力。大厂对代码规范、文档、调试能力、性能分析都有成熟要求。客户端开发尤其看重崩溃排查能力——dmp文件分析、Windbg调试、内存泄漏检测这些技能才是客户端工程师值钱的地方因为线上问题一旦发生你需要在用户机器上远程定位而不是靠本地复现。所以我对这个岗位的判断是它不是市面上那种“会做界面就行”的客户端岗位而是以系统级稳定性为核心、以安全攻防场景为驱动的高门槛研发岗。你在准备时如果只准备了MFC或Qt界面开发那是不足以覆盖面试深度的必须往系统底层和防御对抗方向扩展。2. Windows客户端核心技术栈从界面到底层的完整清单聊完岗位定位下面把Windows客户端开发领域真正会被考察到的技术点做一个完整拆解。这一节内容比较多你可以把它当成一张查漏补缺的清单。2.1 界面层框架选型和消息机制的底层逻辑客户端开发必然涉及界面。2020年那会儿奇安信这类安全公司Windows端的主流方案还是MFC和Qt并存。MFC老但稳很多遗留的桌面安全工具都是MFC写的Qt则在新项目中占比越来越高因为跨平台、信号槽机制写起来比MFC的消息映射舒服得多。但不管用哪个框架Win32的消息循环是绕不过去的基础。你要理解Windows应用程序的本质一个消息驱动的系统。鼠标点击、键盘输入、系统通知最终都转化成一个个消息投递到对应窗口的消息队列。窗口过程函数WndProc就是处理这些消息的入口。很多初学者容易忽略的一点是UI线程不要做耗时操作。因为消息循环是单线程的如果你在WndProc里执行了一个耗时很久的同步操作整个窗口就会卡死Windows会判断该窗口无响应Not Responding甚至弹出去强行关闭的对话框。安全客户端的界面卡顿会被客户直接投诉所以这块在实战中特别需要重视。我的经验是凡是超过50毫秒的操作一律丢到工作线程通过PostMessage或Qt的信号槽回到UI线程更新界面。另外界面层还有一个容易被踩的坑是DPI缩放。Windows从Vista开始引入了DPI虚拟化如果你用旧框架写界面在高分屏上会出现字体模糊或控件错位。解决思路有两种要么在exe的manifest里声明DPI Aware让系统不做位图拉伸要么用Qt 5.6以上的版本它原生支持Per-Monitor DPI Awareness。建议直接用Qt省心很多。2.2 系统编程层服务、注册表、文件与进程那些事Windows客户端跑业务逻辑时不可避免要和各种系统组件打交道。我把这一层的技能点拆成几块Windows服务程序。安全软件的主进程通常以服务方式运行这样可以在用户未登录时后台启动也能以系统权限执行高权限操作。服务程序要遵守SERVICE_STATUS的交互协议正确处理SERVICE_CONTROL_STOP、CONTROL_SHUTDOWN等控制码。开发时可以先注册成普通exe跑通逻辑后再用sc.exe或服务管理器注册为服务调试效率会高很多。注册表和WMI。注册表是Windows核心配置的存储仓库客户端经常需要读写Run启动项、服务配置、文件关联这些键值。WMI则用来查询系统信息比如CPU序列号、硬盘ID、操作系统版本也是做设备指纹、安全基线核查的常用手段。这里有个实操细节查询WMI时COM初始化一定不能忘否则接口调用会莫名失败。进程和线程管理。安全客户端必然要枚举进程、获取进程路径、加载模块列表甚至注入DLL做行为监控。常见的进程枚举API有CreateToolhelp32Snapshot、EnumProcesses、WTSEnumerateProcesses三种各有适用场景。如果只看本进程用前两者如果要拿到每个进程对应的用户名WTS系列是首选。文件系统监控。终端安全产品需要实时感知文件落盘变化比如查杀病毒时监控临时目录释放的可疑文件。最底层的实现是文件系统minifilter驱动工作在文件系统层面性能好且不容易被绕过用户态的备选方案是ReadDirectoryChangesW实现简单但精度低、容易被反监控。奇安信这类产品底层的文件监控大概率是minifilter实现的作为一个客户端开发你不一定要写驱动但要明白用户态和内核态的配合关系这样出了问题才能和驱动组的小伙伴流畅沟通。2.3 网络编程与协议交互客户端不是孤岛客户端要和服务端通信上报日志、接收策略、升级版本这些都依赖网络编程。Windows下有IOCP完成端口、select、WSAAsyncSelect、WSAEventSelect几种多路复用机制。客户端场景不建议用select因为效率差且受限IOCP是Windows上性能最优的方案适合高并发连接场景。安全软件的网络通信还有一个特殊要求必须处理网络不可达、DNS解析失败、超时等异常。终端环境比服务器环境复杂得多用户可能在断网状态下长时间使用电脑如果客户端一遇到网络异常就崩或者狂重试体验会很差。我的做法是做成“本地优先”架构数据先写本地队列网络恢复后再批量上报这样既保证可靠性也降低对用户网络的干扰。2.4 安全对抗与系统防护客户端开发的高阶护城河如果面试的是奇安信安全业务的客户端岗安全对抗这块必须有所涉猎。终端的扫描引擎、行为检测、反Rootkit、反勒索大量依赖底层系统机制的理解。以Hook为例安全产品需要监控敏感API调用比如写MBR、修改启动项常见做法是Inline Hook或IAT Hook。Inline Hook的原理是在目标函数入口处改写字节跳转到自己的检测函数执行完再跳回原函数。这期间要考虑指令对齐、锁粒度、递归调用等一堆细节调试起来非常痛苦。但如果不做Hook就无法感知恶意行为所以这是安全客户端的必修课。还有进程保护。安全软件为了防止被杀毒软件互杀或恶意程序强杀会对自己进程做保护比如设置调试权限、注册系统回调、R3层拒绝打开句柄。当然安全软件之间的对抗是个永恒的话题这里不展开细节但要明白这是“终端安全”的一部分业务逻辑。2.5 性能分析与崩溃排查客户端开发者的系统工程最后是排查能力这一节单独说因为它是决定你能否在线上环境存活下来的关键。你在开发过程中写的功能再怎么完善一旦到内外网海量用户环境下跑出问题就是灾难。崩溃、卡死、内存增长、句柄泄漏——这些问题的排查依赖一套完整的工具链。我的习惯是给日志和dmp文件留足后路。所有发布版本必须打开PDB编译选项上传符号表到符号服务器测试包崩溃后用集成好的CrashRpt或Google Breakpad自动生成minidump然后离线分析。分析dmp文件用WinDbg分析内存泄漏用Application Verifier配合UMDH分析句柄泄漏用Process Explorer。这套流程熟练之后线上问题的定位效率能提升一个量级。3. 实操从零搭一个带日志上报的Windows客户端服务讲了这么多理论层面还是要落到代码上。我模拟一个安全客户端会遇到的典型任务写一个驻留后台的资源监控服务定时采集系统数据上报到服务器并且要带好日志模块、异常捕获和自动重启能力。这个实操思路基本能覆盖岗位的日常。3.1 场景设计与选型先设计业务需求。我们的Windows服务要完成这几件事每小时采集一次CPU和内存占用、记录文件落盘热区Top10、将数据以JSON格式上报HTTP接口。考虑到服务要保持轻量不强依赖UI我直接用VS2015创建Windows服务项目开发语言用C第三方库只引入一个libcurl用于HTTP上报。整个工程的WPF/MFC都不需要因为服务不允许有UI。这里插一句为什么选VS2015。虽然2020年VS2019已经很成熟但很多安全厂商的内部构建环境还停留在老版本VS2015生成的exe在Win7到Win10的兼容性矩阵中更稳。用VS2015不是技术落后而是客户端产品在兼容性上必须保守。如果你手里只有新版本编译器可以通过Platform Toolset方式指定v140工具集来模拟老环境编译。3.2 服务框架实现正确处理SERVICE_STATUS状态机Windows服务的入口点不是main函数而是服务入口ServiceMain。在ServiceMain里要调用RegisterServiceCtrlHandlerEx注册控制处理器然后向SCM服务控制管理器报告服务状态。服务启动后主逻辑通常放在一个专门的线程里跑这样控制请求停止、暂停、继续能及时响应。下面给出一个精简的服务骨架代码控制在能说明问题的范围#include windows.h #include string #include thread #include atomic #include curl/curl.h SERVICE_STATUS g_status {0}; SERVICE_STATUS_HANDLE g_statusHandle 0; std::atomicbool g_running(true); BOOL WINAPI ServiceCtrlHandler(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context) { switch (control) { case SERVICE_CONTROL_STOP: case SERVICE_CONTROL_SHUTDOWN: g_running false; g_status.dwCurrentState SERVICE_STOPPED; SetServiceStatus(g_statusHandle, g_status); return NO_ERROR; default: return NO_ERROR; } } void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) { g_statusHandle RegisterServiceCtrlHandlerExW(LMonitorSvc, ServiceCtrlHandler, NULL, NULL); g_status.dwServiceType SERVICE_WIN32_OWN_PROCESS; g_status.dwCurrentState SERVICE_RUNNING; g_status.dwControlsAccepted SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; SetServiceStatus(g_statusHandle, g_status); std::thread worker([]() { while (g_running) { // 1. 采集数据 // 2. 生成JSON // 3. 上报服务器 Sleep(60 * 1000); } }); worker.detach(); } int wmain() { SERVICE_TABLE_ENTRYW table[] { { LMonitorSvc, ServiceMain }, { NULL, NULL } }; StartServiceCtrlDispatcherW(table); return 0; }这段代码需要注意几个点RegisterServiceCtrlHandlerExW的第四个参数可以是上下文指针传NULL也可以正常使用worker线程在收到停止信号后要优雅退场不要强杀线程服务被SCM停止后再启动要确保状态重置干净。3.3 数据采集逻辑性能计数器与文件热区统计采集层我用PDHPerformance Data Helper来拿CPU和内存数据。PDH是Windows自带的性能计数器接口比直接读注册表性能数据可靠得多。采集CPU占用的示例代码如下#include pdh.h #pragma comment(lib, pdh.lib) double GetCpuUsage() { static PDH_HQUERY query NULL; static PDH_HCOUNTER counter NULL; PDH_FMT_COUNTERVALUE value {0}; if (!query) { PdhOpenQueryW(NULL, 0, query); PdhAddCounterW(query, L\\Processor(_Total)\\% Processor Time, 0, counter); } PdhCollectQueryData(query); Sleep(200); PdhCollectQueryData(query); PdhGetFormattedCounterValue(counter, PDH_FMT_DOUBLE, NULL, value); return value.doubleValue; }这个代码是典型的两步采样法先采集一次基准值间隔一小段时间再采一次才能计算差值。如果你只采样一次得到的永远是0或者上一个间隔的累计值。这是很多新手写性能计数器容易翻车的地方。文件热区统计可以用ReadDirectoryChangesW监控指定目录比如临时目录和下载目录把文件路径和大小存进一个环形队列定时统计Top10。Windows对ReadDirectoryChangesW的缓冲区有个坑当监控的目录变化非常频繁时缓冲区会溢出API会返回ERROR_NOTIFY_ENUM_DIR这时候你可以调用ReadDirectoryChangesW带FILE_NOTIFY_CHANGE_LAST_WRITE等长标志但更推荐的是干脆把队列做大或者监控粒度细化到具体子目录。3.4 上报逻辑libcurl的选型和超时处理上报用libcurl。libcurl在Windows下编译有几种方式最简单的省事方案是直接用vcpkg安装vcpkg install curl然后链接curl.lib。用的时候记得全局初始化#include curl/curl.h #pragma comment(lib, libcurl.lib) bool UploadJson(const std::string json) { CURL* curl curl_easy_init(); if (!curl) return false; std::string response; curl_easy_setopt(curl, CURLOPT_URL, https://your-server/api/collect); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str()); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 3000L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 内部环境可用公网不要关 CURLcode res curl_easy_perform(curl); curl_easy_cleanup(curl); return res CURLE_OK; }这里有两个实战要点。第一是超时设置必须显式配置Windows下libcurl默认超时是0即永久等待一旦网络异常客户端线程直接挂死。第二是——注意我前面提到过的“本地优先”——上报失败时不要无限重试把数据写回本地磁盘队列等网络恢复后再处理。不然用户在断网状态下你每小时重试一次没成功CPU和磁盘就会被拖垮。3.5 日志与自保护别等出问题了才后悔最后一个环节日志和自保护。日志模块我习惯直接用spdlog它是header-only的C库异步写日志性能极好。安全客户端的日志必须做成按天轮转超过大小自动压缩清理否则用户跑一年之后C盘能多出几十GB日志。我在配置里一般设单文件最大5MB保留7天这样既够排查问题又不会占用太多磁盘。自保护方面最基础的是在服务里加一个WatchDog用一个单独的监控线程定时检查主线程状态如果主线程退出超过规定时间就重启服务。同时服务在SCM里配置失败恢复选项比如第一次失败重启服务第二次失败尝试重新运行程序。这些配置可以用sc命令完成也可以写在安装脚本里。实操时还要注意服务进程最好设置成与桌面不交互避免UAC弹窗打断用户。3.6 完整的工程发布经验写到这里整个实操代码的核心链路就通了。编译发布时最好用Release-x64配置同时保证CRT静态链接避免目标机器缺运行库。安装包用WiX Toolset或NSIS打包安装时以管理员权限运行写注册表启动项和服务注册表项。分发前用Process Explorer确认服务没有打开异常句柄用GFlags打开用户态栈回溯跑一轮稳定性测试再发外网。4. 面试问题与排查技巧实录把这些坑提前踩掉最后一部分整理我见过的高频面试问题和日常开发里的真实翻车现场。你可以直接拿这些问题自查能答上来基本就稳了。4.1 高频面试题速查表问题回答要点进程和线程的区别是什么Windows下如何创建线程进程是资源分配单位线程是调度单位CreateThread和_beginthreadex的区别必须提到_beginthreadex维护了CRT的线程局部变量避免内存泄漏什么是DLL注入有哪几种实现方式CreateRemoteThread、SetWindowsHookEx、AppInit_DLLs、注册表劫持等重点讲机制和各自的检测难度Windows消息循环是怎么工作的怎么避免UI卡顿消息队列、GetMessage/PeekMessage的差异、消息分发耗时任务下放线程用PostMessage回传结果怎么排查Windows程序崩溃先看dmpWindbg !analyze -v没有dmp就用Event Viewer查Application日志看异常Code和模块路径什么是IOCP和select的差异是什么IOCP是内核级异步IO模型避免线程上下文切换select是同步多路复用连接数大时差很多智能指针有哪几种各自适用场景unique_ptr、shared_ptr、weak_ptr安全客户端多线程环境下务必减小shared_ptr的捕获范围避免循环引用自己实现一个线程池需要考虑什么任务队列、工作线程数量、空闲线程回收、任务取消、异常隔离面试时可以画结构图别用mermaid直接口述或手绘这里想单独解释一下“DLL注入”这个问题。很多同学背答案背得很熟能说出四五种注入方式但一问到“怎么检测自己的进程被注入了”就答不上来。这说明对注入原理的理解是死的。在安全客户端的场景里检测DLL注入有一个相对简单的思路遍历进程模块列表检查有没有非预期路径的DLL再比对模块签名。如果发现可疑模块可以用Process Explorer加载到符号后看它的导出表。安全对抗的本质就是攻防两端的理解和反制这个思路比背十个注入方式都重要。4.2 内存泄漏的经典排查案例我在实际项目里遇到过一个非常刁钻的内存泄漏服务在运行中内存占用每6小时涨100MB重启恢复泄露非常规律但用Application Verifier又抓不到明显异常。后来用UMDH抓了两份堆对比快照发现增长集中在某个第三方SDK的全局Cache上。这个SDK在每次上报请求时都往全局map里插入一个缓存项但Cache的清空策略只设置了一个很长的TTL72小时导致TTL没到之前内存一直在累积。这个案例给我们的教训是客户端内存优化不能只盯着自己的代码第三方库的默认行为也要审查。尤其是安全厂商经常引入各种引擎库和特征库它们内部如果做了缓存而没有配套的清理策略长期运行必然出问题。思路就是上线前对核心组件做48小时压测观察内存曲线是平稳还是爬坡如果爬坡就用UMDH或者vLDVisual Leak Detector做快照对比逐层抓数据。4.3 服务启动失败一个具体的“找不到模块”案例再分享一个我在Windows服务开发中遇到的典型问题。服务编译好以后在开发机运行正常一旦复制到测试机就提示“服务启动失败错误193%1不是有效的Win32应用程序”。排查了半天发现是测试机缺了VC运行库。因为我的工程用的是动态链接版CRT而测试机只装了精简版系统没有对应版本的msvcr140.dll。这类问题在客户端开发中特别常见。解决思路有两条一是工程属性里把Runtime Library设置成“Multi-threaded (/MT)”即静态链接二是把VC Redistributable集成到安装包中用q:silent参数静默安装。我的建议是两者都做安装包静默装运行库既解决当前机器问题也兼容后续依赖其它动态库的情况。4.4 采集线程卡死的复盘还有一个案例服务上线后某天突然不再上报数据了Log显示“采集线程在GetSystemTimes里阻塞了超过5分钟”。复现后发现是目标机器CPU被某个高负载进程占满系统在极端负载下线程调度延迟极大。GetSystemTimes去读内核计数的时候如果系统恰好在做核心转储或者磁盘IO风暴调用就可能卡很久。这个案例的解法是给所有耗时操作统一套一层超时控制。Windows下没有完美的“线程超时结束”机制常用的做法是把轮询间隔改小用一个看门狗线程检测到数据采集线程超过N秒未返回时先记录现场再触发异常退出并重启采集线程。这里要注意不能直接TerminateThread否则会留下锁泄漏。更优雅的方案是把采集操作改成异步任务加上超时联合体但工程上为了快速止血很多团队会用重启线程并释放局部资源来缓解。4.5 给准备面试的同学三个加分的实操建议结合我作为面试官的习惯给准备这个岗位的同学三个加分动作。第一Windows开发岗位的简历和项目经验一定要写“性能优化”和“问题定位”方面的事例。比如你曾经把某个模块的启动速度从2秒优化到300毫秒或者你在没有源码的情况下通过dmp定位到了第三方库的内存越界点。面试官最欣赏的不是“我做了什么功能”而是“我发现了什么问题用什么样思路定位到最终怎么定量解决的”。第二提前熟悉Windbg的几个常用命令。!analyze -v做自动分析!heap -p -a查看堆分配!address -summary看内存布局kP打印线程调用栈。能够现场用Windbg分析一个简单dmp的同学在候选人里属于前20%。第三准备一个“逆向排查”的案例。比如你经手过一个线上问题最终是通过日志数据反推发现是某个Windows API在特定系统版本上的行为差异导致的。这类案例最能证明你对Windows系统机制的理解不是纸上谈兵而是有大量实机环境的积累。4.6 关于多线程同步、信号量、互斥量、临界区的补充说明面试时还有一个高频考点是Windows同步原语的选型。这个问题看似基础但很多候选人答不出“什么时候用临界区什么时候用信号量”之间的深层区别。我的经验是临界区CriticalSection是用户态锁性能好但只能用于同进程内的线程同步互斥量Mutex是内核对象可以跨进程会有两次上下文切换信号量Semaphore既可以做互斥也可以做事件触发适合协调生产者和消费者。安全客户端的线程模型通常很复杂一个采集线程、一个上报线程、一个看门狗线程、还有一个UI进程间的IPC通信线程。如果全部用一个全局互斥量会出现严重的锁竞争导致采集错过时机。我一般会按数据域的粒度拆分锁比如配置数据用读写锁、内存队列用临界区、跨进程信号用命名互斥量。高并发场景下测试拆锁之后整体吞吐提升了大概40%这是实打实的收益。5. 聊聊技术视野客户端开发视角的边界延展如果你正在准备这个岗位或者已经在做Windows客户端开发我想多聊几句技术视野的问题。2020年那会儿很多人觉得客户端开发是“夕阳方向”前后端才是主流。但站在安全厂商的视角看只要终端设备存在客户端开发就永远是安全产品的核心竞争力之一。从QuestDB这类高性能时序数据库在客户端日志上报中的应用到PHP之类的语言也在做邮件读取客户端之类的业务需求你会发现“客户端”这个词的边界其实很广。安全客户端的核心不是界面而是“如何在宿主环境里稳定地执行任务”。这个核心能力训练出来以后可以迁移到很多方向往底层走是内核驱动、固件安全往上层走是EDR、零信任终端组件往边缘侧走是嵌入式设备客户端。一条稳定的主线是学会在受限、复杂、不稳定的环境中构建可靠服务。这套能力是通用的。另外要说的一点是Windows开发本身在2020年之后也在快速变化。新的操作系统版本带来了更强的安全模型、更严格的默认配置、更丰富的系统API安全客户端的对抗技术越来越依赖内核态用户态代码逐渐被弱化。但你会发现面试官和团队真正看重的能力依然是扎实的C功底、对Windows系统机制的深刻理解以及定位复杂问题的系统工程方法。只要这些基本功在每次系统更新带来的变化都可以快速跟上。我自己在这些年的观察中有一个体会真正能在客户端领域走远的人往往是那种愿意花大量时间读微软文档、写最小复现程序验证系统行为的人。Windows的很多行为都是“文档没有明说但实际就是这样的”这种隐性知识的积累没有捷径只能靠一行行代码、一个个抓包样本去攒。面试时你不需要表现得无所不知但你要让面试官相信你有能力在没人教你的情况下独立找到答案。这本身就是客户端开发者的核心竞争力之一。最后再分享一个实操层面的小技巧如果你手头有Windows环境建议从今天开始给自己布置一个小项目比如用VS2015写一个带服务、日志、内存监控、HTTP上报的最小客户端然后故意制造线上崩溃用Windbg把dmp分析一遍记录一份完整的排查文档。把这段经历和文档写到简历上它的说服力远胜于你背一百道面试题。等你做完这个闭环奇安信这类岗位的面试你面试官问到的绝大多数问题你都已经在实操中遇到过了。