跳转至

2026-06-16

今日主题

  • macOS 文件默认打开方式管理
  • 终端文件路径点击行为
  • Homebrew 分发的软件类型
  • 开发库的文件形态与安装约定
  • pkg-config 与 .pc 文件机制
  • MDM 静默推送软件的追溯方法
  • Go vs Java 文件名与包命名差异
  • C语言编译四阶段与中间产物
  • Go编译过程与C的差异

新增认知

macOS 文件默认打开方式管理

  • duti -x 查扩展名,duti -d 查 UTI:duti 的查询参数极易混淆——-x 接受扩展名(如 json),返回当前默认应用;
    -d 接受 UTI(如 public.json),返回该 UTI 的 handler 信息。之前一直用 duti -d json 查扩展名,
    UTI 名为 json 的注册项不存在,所以始终返回 no default handler,误以为设置未生效。正确验证方式是用 duti -x 而非 -d。

  • duti -s 无报错不代表写入成功:duti -s 返回 exit 0 只表示命令执行无异常,
    不保证 macOS LaunchServices 实际接受了绑定。必须用 duti -x 验证。
    应用还需在 Info.plist 的 CFBundleDocumentTypes 中声明对应 UTI 或通配符 *,否则系统拒绝绑定。

  • Shell 双引号内 ~ 不展开:~ 在双引号中保留字面值,不会展开为 HOME 目录。如 ~/path 在双引号内变成字面字符串 ~/path,
    正确写法是使用 $HOME 变量或把 ~ 放在引号外。
    之前 zshrc 中 idea_bin="~/Applications/..." 因此一直指向不存在的路径。

终端文件路径点击行为

  • iTerm2 点击文件路径打开编辑器是终端行为,非 Claude Code 控制:普通终端模式下,
    iTerm2 的智能选择(Smart Selection)功能识别 file.ts:123 模式,Cmd+Click 时调用 macOS 系统默认应用打开,
    与 Claude Code 无关。仅全屏 TUI 模式(/tui fullscreen)下 Claude Code 才接管鼠标事件自行处理。
    当前环境为 default 模式,/tui 命令可查看当前模式。

  • macOS 文件默认应用由 LaunchServices 决定,按扩展名关联
    系统通过 UTI(Uniform Type Identifier)映射扩展名到应用。应用在 Info.plist 中声明支持的 UTI 和扩展名,
    用户可通过 Finder 简介面板或 duti 命令行工具覆盖默认绑定。批量修改需逐扩展名设置,
    .html 受 public.html UTI 系统保护限制无法通过 duti 覆盖。

Homebrew 分发的软件类型

  • Homebrew 分发六类软件:Homebrew 不只装 CLI 工具,
    还可以装 GUI 应用(Cask)、开发库(.a/.dylib + .h)、语言运行时、后台服务(brew services 管理)、字体。区分方式:
    装完有可执行文件是 CLI/运行时;装到 /Applications 是 GUI;只有头文件和链接库是开发库;
    可以 brew services start 的是服务。

  • Cask 与 Formula 的本质区别:Formula 会走编译或预编译安装流程,结果是通用 Unix 文件布局;
    Cask 专用于 macOS 二进制分发,直接把 .app 拖进 /Applications 或解压预编译包,不做编译。

开发库的文件形态与安装约定

  • 开发库 = .a/.so/.dylib + .h
    开发库安装产物是静态库(.a)、动态库(macOS .dylib / Linux .so)和头文件(.h)。它不提供可执行文件,
    用途是在编译其他程序时 #include 头文件并 -l 链接,本身没有用来运行的场景。

  • Homebrew 隔离在自己的 prefix:Homebrew 遵循 Unix {bin,lib,include,share,etc} 约定,
    但整体放在 /opt/homebrew(Apple Silicon)或 /usr/local(Intel),不污染系统目录 /usr。
    编译时需要用 -I/-L 显式指向 Homebrew prefix,或交给 pkg-config 自动解析。

pkg-config 与 .pc 文件机制

  • pkg-config 解决库路径可移植性问题:不同平台库路径不同(/opt/homebrew vs /usr),手写 -I/-L 路径不可移植。
    库提供 .pc 文件(记录 prefix、Cflags、Libs),pkg-config 读取后输出正确参数,
    编译命令统一为 gcc $(pkg-config --cflags --libs foo) myapp.c,跨平台无需改动。

  • 并非所有库都有 .pc 文件:pkg-config 支持是可选的,
    库维护者需主动提供 .pc 文件并安装到 PKG_CONFIG_PATH 下(如 /opt/homebrew/lib/pkgconfig/)。
    没有 .pc 文件的库只能手动指定路径,libyaml 提供了 yaml-0.1.pc 所以可以直接用 pkg-config 查询。

MDM 静默推送软件的追溯方法

  • MDM 可静默推送安全软件:公司 MDM 策略(如得物通过飞连)可以自动安装 DLP 等安全软件到员工设备,无需用户手动操作。
    通过 profiles status 查看 MDM 服务器域名可确认归属,
    pkgutil --pkgs 和 pkgutil --pkg-info 可查安装包及时间。

  • 安全软件常用混淆文件名:sys5c2gpr0 这种无意义随机名是 DLP 软件的反检测手段,即使文件名无意义,
    codesign 仍能暴露真实签名方(这里是 CirrusGate)。

  • 追溯未知二进制文件的标准流程
    file 看类型 → codesign -dvvv 看签名方 → otool -L 看链接库 → pkgutil 看安装包 → profiles status 看 MDM 来源 → 查找 LaunchDaemons/Agents 看启动方式,
    这五步能完整还原一个未知二进制文件的来源和用途。

Go vs Java 文件名与包命名差异

  • Go文件名与语���语义无关:Go 的编译单元是 package(目录),文件名只是文件系统组织方式,不参与语义,
    所以文件名可以是关键字(如 select.go)。Java 则要求文件名必须与 public 类名一致,类名是标识符不能用关键字,因此文件名也被间接限制。

  • Go同目录文件共享符号表:同一 package 下所有 .go 文件编译时符号表合并,跨文件可直接引用顶层声明,无需 import,
    也没有跨文件的声明顺序要求。Java 则文件是独立编译单元,互相引用必须 import。

  • Go internal目录有编译器强制语义:Go 的 internal/ 目录下的包只能被其父级模块引用,这是编译器强制的,不是约定。
    Java 无此机制,内部包可见性只靠访问修饰符。

C语言编译四阶段与中间产物

  • C编译四阶段产物:预处理(.c→.i,文本展开 #include/#define)→ 编译(.i→.s,生成平台相关汇编,
    经过 IR 优化)→ 汇编(.s→.o,ELF 格式目标文件)→ 链接(多个.o/.a→可执行文件,解析符号+重定位)。.s 已是平台相关的,
    但作为可观测中间层便于调试编译器优化。

  • ELF目标文件的重定位机制:.o 文件里跨文件的地址引用全是占位符(0x00000000),包含重定位表记录哪些地方需要修正。链接器做符号解析后,
    把占位符替换成真实虚拟地址。ld 只决定虚拟地址布局写入 ELF,真正映射到进程内存是运行时 loader 的工作——两者职责不同。

  • 静态库.a按需提取.o:.a 是多个 .o 的打包(ar 格式),附带符号索引。链接器用到 .a 时不是整个打进去,
    而是查索引找到需要的符号在哪个 .o,只提取那几个 .o。所以静态链接是按需提取,不是全量复制。

  • main符号报错的本质:ld 找不到 main 报 undefined reference 不是因为 ld 特别关心 main,
    而是 C 运行时 crt0.o 提供的 _start 引用了 main 符号,符号解析失败就报错,和其他 undefined reference 性质完全一样。

Go编译过程与C的差异

  • Go无预处理阶段:Go 没有宏和 #include,//go:build 是编译器内置的文件级过滤(不是文本替换),
    stringer 是 go generate 触发的代码生成工具与编译完全分离。所以 Go 编译只有:
    编译(go tool compile)→ 链接(go tool link)两个显式阶段。

  • Go按依赖拓扑顺序编译package:编译不是最终一次链接,而是拓扑排序后逐个编译 package,每个 package 产出 .a。
    下游 package 编译时需要读上游的 .a 获取类型信息做类型检查,所以上游必须先编译完。这也是 Go 不需要头文件的原因——类型信息打进了 .a。

  • Go的.a和C的.a用途不同:C 的 .a 只在链接阶段用(提供机器码)。Go 的 .a 在编译阶段就要用(提供导出类型信息做类型检查),
    链接阶段再用(提供机器码)。一个文件承担了 C 里头文件+静态库两个角色。

  • Go编译一个package:多文件一次调用:go tool compile 把同一 package 下所有 .go 文件作为整体一次传入,
    内存里合并符号表后统一类型检查,各函数独立生成机器码,最终打包成一个 .a。内部可能存在类似 .o 的中间结构但不落盘,
    C 里 .o 落盘是因为工具链分成独立程序必须用文件传递。