LDS实习首月总结

工作总结

  • 对清理大师安装卸载界面进行高dpi适配.
  • 学习DLL修复的项目代码, 重点学习DLL自学习相关部分的代码并进行调试, 找到了一个路径问题, 一个大小写匹配的问题.
  • 实现DLL修复中自修复功能从独立版到内嵌版的迁移.

知识总结

环境配置

公司主要是用VS写项目, 遇见了很多之前没有遇到的情况, 下面把一些知识点或情况总结一下 :

  • 可以用.prpos属性表来进行项目配置的导入, 将同一个属性表拉到多个项目下, 可以让项目有相同的配置, 这种方式属于另类覆盖, 有新的用新的, 只有旧的用旧的.
  • 想要导入第三方库, 需要配置头文件和库文件.
    • 附加库目录(.lib):项目属性 → 链接器 → 常规 → 附加库目录.
    • 附加包含目录(.h): 项目属性 → C/C++ → 常规 → 附加包含文件.
  • DLL动态库实际在生成时也会产生配套的.lib文件, 这叫做导入库, 可以理解为lib静态库中就是实打实的代码, lib导入库中是动态库的各种函数信息之类的, 只有导入了导入库才可以实际使用动态库.
  • 动态库文件.dll会在程序启动时有操作系统进行搜索, 搜索路径如下 :
    • 应用程序所在目录(即生成的 .exe或主程序所在文件夹)
    • 系统目录(如 C:\Windows\System32)⚠️ 避免使用:可能引发版本冲突或安全问题。(但实际我看确实有用到)
    • Windows 目录(如 C:\Windows
    • 环境变量 PATH中的目录 适合全局依赖的库(如显卡驱动相关的 .dll)。
  • 导入表是PE文件中记录该模块需要从其他DLL中导入哪些函数的数据结构。它告诉系统加载器:”我需要这些DLL中的这些函数才能运行”。
  • 要善用F12和ctrl + -来实现函数跳转.
  • 文件全局搜索也很有用.
  • 命令行参数可以理解为一种高级的函数参数, 一个程序可以通过命令参数改变自己实际执行的行为, 让一个程序在不同参数传入下的功能有联系但更多样, 而非拆分出一些其他的项目, 让项目体积变大且逻辑松散. 可以在属性中配置开始时传入的命令行参数, 从任务管理器和cmd中也可以获取运行中进程的命令行参数.
  • 一个解决方案中又多个项目的原因肯定是因为有联系, 但是项目性质可以完全不同, 有main入口的都是标准exe程序, 有动态库静态库的项目, 这些项目一般是主程序会用到, 在编译构建时要先编译, 然后主程序去加载. 编译顺序在属性中可以设置.
  • 动态库分静态加载和动态加载, 静态加载就是上面说的, 要**.h + .lib + .dll**, 在启动时就会加入. 动态加载有懒汉思想, 可以用在会用但用的少的第三方功能, 调用LoadLibrary加载, 首先会去LoadLibrary知道的加载路径去找dll, 如果没有再继续前面的搜索逻辑, 并且动态加载还有一个明显的优势就是不需要头文件, 可以通过动态加载直接获取函数指针, 即拿即用.
  • 静态加载肯定会用到导出表.def, 用来表示要导出的函数接口, 如果不写会自动生成, 但是手写需要在属性中设定模块定义文件, 但是手写可以手动控制导出的接口, 可以控制对外的接口.
  • 如果直接在设置项目配置会造成对配置表的修改, 不要通过git上传这些修改. 视图中存在属性管理器, 每个项目都有对应的Win32.user/x64.user, 这个配置表是用户独有的, 只会作用在当前环境, 不会影响公共配置.

调试

  • 消息循环内部无法调试, 进入消息循环会进入无法调试的情况, 但是可以在回调函数处设置断点一旦进入回调就会进入可调试状态.
  • 多线程下主线程还是普通黄箭头, 其他线程都会在右下角加蓝色惊叹号标记.
  • 如果碰到新开进程情况, 可以把命令行参数持有, 然后去对应项目设置命令行参数继续调. 没有直接跳到新开进程的说法.
  • 如果想调试一个需要生产环境的代码, 可以找到你安装的地方, 然后将exe文件和动态库的输出目录在属性中设定好, 可以拿到对应的动态库就可以正常调试了.
  • 如果想调试一个动态库代码, 一种方式是在一个解决方案下的另一个exe项目中动态加载它, 并且声明引用该项目, vs就会自动以该exe为启动该动态库的exe, 在代码中设置断点, 当exe中执行到该动态库代码时就会进入调试.
  • 另外一种方式是确定有一个exe在代码中会动态加载并使用该动态库, 然后该动态库命令的项目属性->调试->命令 中设置该exe的路径, 并设置该动态库为启动项目, 就会执行该exe, 运行到动态库代码的时候就会进入调试. 当然以上的前提是exe是Debug版本编译的.

Git

  • git确实很有用, 并且以前从来没有用过分支, 但这次就实际使用过了, 一般都是先做先遣版本分支, 提测完好后再合并到主分支中.
  • 确实有使用很多git网站, 比如gitlab, 云效之类的, 确实也很有东西, leader可以控制解锁成员的可用部分.
  • 而且还遇到了代码冲突的情况, 这里会让你选择冲突的两个版本之一, 之后删掉其一后编译通过再add+commit即可.
  • GIT远程分支主要包含:master develop feature release fixbug
    • master: 整个项目主分支,有且仅有一个,除项目负责人以外的开发人员不能像master分支合并内容。
    • develop: master只用来分布重大版本,日常开发应该在另一条分支上完成。开发用的分支为develop。
    • feature: feature是为了开发后续版本的功能,从develop分支上面分出来的。开发完成稳定后,要再并入develop。
    • release: release是发布正式版本之前(即合并到master分支之前),我们可能需要有一个预发布的版本进行测试。
    • fixbug: fixbug分支是从master分支上面分出来的。fix结束以后,再合并进master和develop分支。最后,删除”fixbug分支”。
  • 有用到各种各样的git平台, 有gitlab, codeup等等, 可以到时候去了解了解.
  • 想要直接拉取目标分支的代码需要使用 -b 分支名.
  • 切换分支的时候未推送的修改将会全部丢失, 但是可以通过Stash(暂存)功能把当前的修改全部暂存下来, 然后切换分支, 使用Apply将这些修改应用在新的分支上.
  • 如果想将master分支上的修改放在another上提交, 可以先 add + commit 把代码提交到本地仓库, 这样在本地就有了一套新代码存在, 然后切换到another, 再调用git merge master, 就可以将master在本地的版本合并到another上, 编译通过后push即可. 这是一种很正常的用master版本在another开新版本的方式, 用于后续继续开发, 但前提是another要是master的祖先版本, 两个分支同根同源, 要不然也没有这种操作的必要了.

代码规范

copy_right / 命名规范 / do while(false)替代try-catch / 把和类没有关联的独立代码分到Utils中 等等, 确实优秀的代码规范可以让代码可读性变得很高. 虽然我刚接触还是看得很麻烦, 但是确实可以感觉到要是熟悉了流程可以读的很舒服.

而且要求遵守谷歌代码规范, 感觉还是很有用的, 但是要长期适应去匹配, 不是一下就能都改正的.

在vs中注释方式得当的话, 对于读代码是很有帮助的, 尤其是vs会读取注释显示在变量简介中, 而且用prama之类的语法可以细化到函数参数的简介.

日志

感觉也就是那样, 不过一开始的认知是通过日志的提示消息去查BUG, 现在发现其实更多是通过对比不同情况下的日志来发生问题, 而且日志结束的位置也很有参考性.

高DPI适配

这个还是相对简单一些的, 主要原理就是屏幕像素点数不同, 普通台式较少, 因此缩放比设置一般是100, 我的笔记本就高一些, 是125, 一些4k屏可以到达175/200缩放比的情况. 如果没有进行高DPI适配, 那么软件就会被window自适应拉伸, 原本在100下很清晰的图片在200下被拉伸两倍, 会有明显的模糊感, 高DPI适配就是获取屏幕的DPI, 根据dpi计算缩放比, 根据不同的缩放比设置不同的尺寸/字体/图片.

比较普通就是根据程序开始时的DPI设置, 做一次就够了. 如果要根据DPI变化实时改变, 应该要有监控机制, 并且还有多屏幕DPI不同的问题, 这估计也要配套监控机制.

DLL修复项目学习

主进程项目

本项目负责核心逻辑, 包括扫描, 搜索, 修复以及前端界面交互.

这里不好放公司项目的设计与代码, 就不放出了, sorry.

其他记录

系统接口感受

其实仔细了解Windows给上层提供的接口是非常多的, 有很多途径你可以获取各种软件包括硬件的信息, 而这我认为就是lds的生存空间. 我其实认为这些内容也是比较有趣的, 比如说你可以监控捕获系统弹窗读取信息, 当然也可以捕获各种各样的窗口, 那么你就可以对于某些窗口有比较实际的反应与操作. 你也可以读取成功运行程序的花名册, 我读的项目中就用这当作证明dll存在的标志. 你还可以在底层运行很多以前要在表面进行的操作, 比如安装程序, 完全可以对某些安装程序设置静默安装加后台运行, 就可以无声无息实现很多事情.

插件化开发

可以感觉到lds在开发软件时很多都在遵循一套插件化开发的规则, 程序一般分为后台进程和前台进程, 前台进程负责程序核心逻辑, 后台进程负责实现监控任务, 弹窗推广, 插件运行等等, 其实有时候后台进程发挥的作用比前台更重要. 这里面的插件化开发很有意思, 你可以很简单地理解插件就是动态库, 后台进程tray会加载并启动这些插件, 他们自己干自己的事情. 实现这种tray的前提在于, 插件需要有约定俗成的同意的接口, 包括init/start/stop等接口, 也要有相同的导出函数, 这样插件才可以用相同的操作启动不同的插件. 而且最重要的应该就是插件是可复用的, 想用就可以拉过来加上.

内嵌式网页前端

用到的技术貌似叫做CEF, 其本质是把chrom浏览器内嵌到桌面应用中, 后端可以通过发送js命令或其他方式实现对于前端界面的变化, 这种技术目前来看是非常先进且主流的(这样看qt貌似有点路边一条…, 优势只在于性能和跨平台了), 可以用CEF + JS实现(微信桌面端就是这种), 我搜索后发现有一种叫Election的技术(网易云就是这种), 也是类似的技术方向, 使用也比较方便, 总之这确实是一种前后端分离, 和让前端界面更好看的一种路线.

适配32位机

普通工具类/管理类应用如果想适配32位机其实直接做X86版本的项目就行, 因为64位机天然兼容32位程序. X86程序唯一的劣势可能在于内存分配会相比X64少一些, 如果对于内存和性能非常敏感的化需要同时做X86和X64.

  • 32位程序 访问 C:\Windows\System32时,会被 自动重定向C:\Windows\SysWOW64

  • WOW64 主要重定向以下关键系统目录:

    32位程序访问的路径 实际访问的路径
    C:\Windows\System32 C:\Windows\SysWOW64
    C:\Program Files C:\Program Files (x86)
    C:\Windows\Sysnative(特殊) C:\Windows\System32(绕过重定向)
  • 另外注册表也分有32位视图和64位视图, 32位程序访问注册表会自动用32位视图访问.

  • 禁用重定向 : 如果我们想访问64位系统目录, 毕竟还是有很多X86的应用的, 就必须启用重定向.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    DisableWow64FsRedirection::DisableWow64FsRedirection() {
    HMODULE kernel_handle = GetModuleHandle(L"kernel32.dll");
    if (kernel_handle) {
    disable_redirection_pfn_ =
    (PFN_Wow64DisableWow64FsRedirection)GetProcAddress(
    kernel_handle, "Wow64DisableWow64FsRedirection");
    revert_redirection_pfn_ = (PFN_Wow64RevertWow64FsRedirection)GetProcAddress(
    kernel_handle, "Wow64RevertWow64FsRedirection");

    if (disable_redirection_pfn_ && revert_redirection_pfn_) {
    disable_redirection_pfn_(&old_value_);
    }
    }
    }

    DisableWow64FsRedirection::~DisableWow64FsRedirection() {
    if (disable_redirection_pfn_ && revert_redirection_pfn_) {
    revert_redirection_pfn_(old_value_);
    }
    }

    这里可以看到是通过加载动态库kernel32.dll调用其中的禁用函数来实现的.

SQLite3

这几乎是所有客户端项目需要用到的数据库, 所有数据全部来自数据库请求是很笨重的, 因此客户端一定要有自己的数据库存储数据.

  • MySql本身资质非常优秀, 但是需要独立进程, 需要配套服务器, 作为客户端简单存储是不合适的.
  • 但是SQLite3本身非常简单, 只需要动态加载sqlite3.dll, 再配合xxx.db文件, 就可实现数据在客户端上的存储.
  • 貌似可以服务器使用Mysql, 然后客户端使用SQLite3, 有关客户端的db更新 :
    • 公司项目中应该是客户端从服务器直接下载新的db, 然后替换掉原本旧的db, 会设置类似一周左右的时间, 过了时间才会发布服务器中新版本的db, 以免访问服务器频繁. 但这其实只适用于实时性不高的情况, 因为设置了1周的更新间隔.
    • 如果实时性高貌似可以写一套合并机制, 服务器记录客户端上次的下载版本, 在客户端下次申请时根据版本号的差距, 只传输发生变动的数据, 去修改客户端的数据库, 这样就不必下载了, 但这应该还会复杂更多.

额外知识

  • DevOps表示 “开发 + 运维一体化”, 我认为可以简单理解为长期的高频更新的由一个团队维护一个项目, 从构建到上线到维护.

  • CI/CD , 持续集成/持续交付, 是一种模式, 后续还可以深入理解.

  • C++ 用 智能指针 + lambda删除器 的方式可以很好地实现对于Windows句柄的RAII化.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    auto handle_deleter = [](HANDLE handle) {
    if (handle != NULL && handle != INVALID_HANDLE_VALUE) {
    CloseHandle(handle);
    }
    };
    std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(handle_deleter)>
    hmap_file(CreateFileMapping(hfile.get(), NULL, PAGE_READONLY, 0,
    file_size, NULL),
    handle_deleter);
  • PE(Portable Executable) 是 Windows 操作系统下的可执行文件格式,用于存储 EXE(应用程序)、DLL(动态链接库)、SYS(驱动程序) 等二进制文件。每个PE文件都有DOS头和NT头, 可以通过读这两个头去验证PE文件, 尤其是NT头中存有入口点, 节表数量等数据, 是找到导入表的关键信息.

  • std::filesystem::path absolute :

    其作用是把相对路径转化为绝对路径, 可以用来把一个绝对路径和另一个相对路径拼接.

    1
    2
    3
    4
    5
    6
    7
    8
    std::wstring ResolveRelativePath(const std::wstring& absolute_path,
    const std::wstring& relative_path) {
    std::filesystem::path exe_dir = std::filesystem::path(absolute_path);
    // 将绝对路径与相对路径拼接
    std::filesystem::path target_path =
    std::filesystem::absolute(exe_dir / relative_path);
    return target_path.wstring();
    }

    如下 :

    1
    2
    3
    4
    absolute_path = "C:\Programs\MyApp\"
    relative_path = "..\config\settings.json"
    exe_dir / relative_path = "C:\Programs\MyApp\..\config\settings.json"(未规范化)
    absolute(exe_dir / relative_path) = "C:\Programs\config\settings.json"
  • std::filesystem :

    这个是C++17中用来管理文件系统的类别. 通常缩写为fs.

    • parent_path : 返回一个文件路径的父目录. (remove_filename类似同效果, 但是会实际修改路径)

    • seace : 传入一个路径, 输出该路径所在卷的总空间capacity和可用空间avaliable.

    • file_size : 传入一个文件路径, 返回这个文件的大小.

    • bool copy(const path& from, const path& to, copy_options options) :

      支持目录复制也支持文件复制.

      选项 描述
      none 默认行为
      recursive 递归复制目录
      copy_symlinks 复制符号链接本身
      skip_symlinks 跳过符号链接
      directories_only 仅复制目录结构
      create_symlinks 创建目标为符号链接
    • / : 这个操作符可以进行对fs::path的动态拼接, 本质等同于append, 但是会智能添加分隔符.

    • 路径转化函数 : 常用共有三种, 用来让路径完全合规.

      • absolute : 将相对路径转换成绝对路径, 本质其实就是取出当前工作目录然后和传入的相对路径拼接, 可以用于转化安装路径.
      • canonical : 和上一种效果相同, 但是上一种不会检查路径是否存在和是否有符号链接, 这种会检查, 一旦查到路径不存在, 会抛出异常.
      • relative : 将绝对路径转化为相对路径, 分别传入两个路径, 目标路径和起始路径, 如果目标路径比起始路径深度深, 会截取目标路径深的那部分; 如果比起始路径深度浅, 会根据浅的层数返回对应数量的”../“, 不过还有两个路径是否处于相同分支的问题, 如果不同还会有对应的截取方法.
    • lexically_normal : 将path内部路径规范化.

      1
      2
      path p = "a/./b/../c//d/";
      p.lexically_normal(); // "a/c/d"
  • 持有一个exe_name去进程中通过进程名查询exe的绝对路径, 其中是有同名的风险的, 并且还不低, 但是如果同时持有exe的md5, 只要进程对应exe的name和md5匹配, 那么这个进程对应的exe的绝对路径极大概率就是正确的.

  • leader经常会用Jenkins这个网站, 貌似是做CI/CD的, 可以之后深入了解.

  • 找进程去任务管理器的详细信息中去找, 再进程中找有可能会隐藏再一些主线程中.

  • 放量 : 假设有100万用户, 放量20万, 就代表有20万用户会用到新功能, 类似于灰度上线.

  • 留存 : 字面意思, 下载了并且没有卸载的量.

  • 代码比对可以使用 Beyond Compare 这个软件, 可以很好地显示前后区别并做交替操作.

  • 要记得经常用ctrl + k + f进行格式化, 不然很可能超过80上限.

一些感想

21日和leader聊天, 如果实力相同的话肯定还是会选择学历高的那一方, 所以既然我已经决定走本科就业这条路, 就一定要让我的实力在一些方面有突破性的优势, 并且可以被感觉. 比如我现在在学Windows桌面软件的后端, 就要总结好WindowsC++开发的细节, 并且深入学习一些不常用的优势点, 比如IOCP, 插件化开发, C++17的应用; 这之外需要深入学习Redis, 学习卡夫卡和rocketMQ, 目的主要还是把自己的眼界和技术栈开阔一些; 可以把学习GO排上日程, 我认为确实有一定意义; 可以考虑看一些之前看过的Github上的C++开源项目, 我认为还是要深入体会C++究竟还能干什么, 更多的了解一些C++插件, 第三方库, 让自己对于C++的使用增添信心, 不能仅仅着眼在工作用什么, 而是让自己有一种所谓的心流, 去感受编程的创造力.


LDS实习首月总结
http://example.com/2025/11/09/LDS实习首月总结/
作者
天目中云
发布于
2025年11月9日
许可协议