【nmap源码】Nmap OS扫描核心技术深度解析:从循环迭代器到随机数生成

Nmap OS扫描核心技术深度解析:从循环迭代器到随机数生成

目录

  1. 循环迭代器:高效遍历未完成主机列表
  2. 执行与状态分离:工业级设计模式
  3. 全局静态变量:性能统计的核心
  4. 拥塞控制结构体:扫描速率的智能调节器
  5. cwnd翻倍机制:慢启动阶段的核心特征
  6. 初始化方法:动态配置拥塞控制参数
  7. TCP拥塞控制四大算法:网络传输的基石
  8. MSS概念:TCP报文的最大数据长度
  9. 时间计算函数:扫描耗时的精确计量
  10. 抓包器初始化:构建高性能抓包环境
  11. 字符串安全拼接:Snprintf详解
  12. 跨平台接口名转换:Windows平台的特殊处理
  13. 随机数生成:加密级安全随机源

1. 循环迭代器:高效遍历未完成主机列表

1.1 核心概念

在Nmap的OS扫描代码中,std::list<HostOsScanInfo *>::iterator nextI 这个"循环迭代器"并非C++标准库的原生概念,而是基于标准库迭代器实现的业务逻辑层面的功能

本质特征:

  • nextIstd::list 的普通迭代器,用于遍历 incompleteHosts 链表容器
  • "循环"是指:当迭代器遍历到链表末尾(end())时,会自动回到链表开头(begin()),实现循环遍历未完成扫描的主机列表

1.2 核心方法实现

cpp 复制代码
// 重置迭代器到链表开头
void OsScanInfo::resetHostIterator() {
    nextI = incompleteHosts.begin();
}

// 获取下一个未完成主机(核心:循环遍历)
HostOsScanInfo *OsScanInfo::nextIncompleteHost() {
    // 如果链表为空,直接返回空
    if (incompleteHosts.empty()) {
        return nullptr;
    }

    // 保存当前迭代器指向的主机
    HostOsScanInfo *host = *nextI;

    // 迭代器后移
    ++nextI;

    // 关键:如果迭代器到末尾,回到开头(实现"循环")
    if (nextI == incompleteHosts.end()) {
        nextI = incompleteHosts.begin();
    }

    return host;
}

1.3 使用场景与价值

在OS扫描类中,循环迭代器的核心价值体现在:

  1. 轮询未完成主机:扫描任务通常需要轮询未完成的主机(比如多线程扫描时,每个线程从列表中取一个主机扫描)
  2. 避免重复遍历:避免每次遍历都从头开始,而是"接力式"遍历,提升扫描效率
  3. 动态适应 :即使链表中的主机被移除(removeCompletedHosts()),迭代器仍能通过重置/重新遍历保持逻辑正确

1.4 设计优势总结

特性 优势
私有化nextI 避免外部直接修改迭代器,保证遍历逻辑的安全性和一致性
循环机制 实现对未完成主机的循环、无遗漏遍历
重置能力 通过resetHostIterator()保证遍历的起点可控

2. 执行与状态分离:工业级设计模式

2.1 设计理念对比

不好的设计(状态耦合) 好的设计(执行与状态分离)
类 = 扫描逻辑 + 扫描状态 类 = 纯扫描逻辑;状态单独存在 HostOsScanStats
一个 HostOsScan 实例只能处理一台主机的扫描 一个 HostOsScan 实例可以复用处理多台主机的扫描
状态存在类成员里,多线程/多主机场景易冲突 状态通过参数传递,每台主机的状态独立隔离

2.2 核心优势详解

2.2.1 无状态化设计,支持实例复用

不好的设计:

cpp 复制代码
class HostOsScan {
private:
    std::vector<Probe> probes; // 每个实例只能存一台主机的探测包
    std::string targetIp;      // 只绑一个IP
public:
    HostOsScan(std::string ip) { targetIp = ip; }
    void sendProbe() {
        send_tcp_probe(probes[0], targetIp);
    }
};

// 扫描100台主机,要创建100个实例,浪费!
HostOsScan scan1("192.168.1.1");
scan1.sendProbe();
HostOsScan scan2("192.168.1.2");
scan2.sendProbe();

好的设计:

cpp 复制代码
// 专门存状态的类:只装数据,不干活
class HostOsScanStats {
public:
    std::vector<Probe> probes; // 某台主机的探测包
    std::string targetIp;      // 某台主机的IP
};

// 通用引擎类:只干活,不存具体主机的状态
class HostOsScan {
public:
    void sendProbe(HostOsScanStats *hss) {
        send_tcp_probe(hss->probes[0], hss->targetIp);
    }
};

// 只创建1个引擎,扫100台主机只需100个状态对象
HostOsScan scanEngine; // 通用引擎,只创建一次

HostOsScanStats host1;
host1.targetIp = "192.168.1.1";
host1.probes = {probe1, probe2};
scanEngine.sendProbe(&host1); // 扫主机1

HostOsScanStats host2;
host2.targetIp = "192.168.1.2";
host2.probes = {probe3, probe4};
scanEngine.sendProbe(&host2); // 扫主机2
2.2.2 状态隔离,避免并发问题

OS扫描通常是多线程/多进程的(同时扫描多台主机),状态耦合的设计会导致严重问题:

  • 不好的设计 :如果多个线程共用一个 HostOsScan 实例,会同时修改类内的 probes 成员,导致数据错乱
  • 好的设计 :每台主机的状态(HostOsScanStats)是独立的,即使多线程调用同一个 HostOsScan 实例的 sendProbe(),只要传入不同的 hss 指针,状态就不会互相干扰
2.2.3 职责单一,代码易维护/扩展

软件工程的"单一职责原则"要求:一个类只做一件事。

  • 不好的设计HostOsScan 既负责"怎么扫描(构建包、发送、处理响应)",又负责"存扫描数据(probes、响应结果)"
  • 好的设计
    • HostOsScan 只专注于"扫描逻辑的执行"
    • HostOsScanStats 只专注于"存储单主机的扫描状态"
2.2.4 状态可灵活管理(持久化/恢复)

扫描过程中可能需要暂停、恢复、保存扫描进度:

  • 不好的设计 :状态和执行逻辑绑死在 HostOsScan 实例里,要保存进度需要从类的各个成员中提取数据
  • 好的设计 :所有状态都集中在 HostOsScanStats 中,只需序列化/反序列化这个对象,就能轻松实现扫描进度的保存和恢复

2.3 生活化类比

HostOsScan(扫描执行引擎)和"状态"的关系,类比成 "外卖骑手"和"订单信息"

  • HostOsScan = 外卖骑手(核心能力是"会送外卖")
  • 扫描状态(probes/响应/速率等)= 订单信息(收货地址、菜品、配送时间、备注等)
  • 多台待扫描主机 = 多个外卖订单

不好的设计 :给每个订单配一个专属骑手,且这个骑手脑子里只记这一个订单的信息
好的设计:一个骑手可以送多个订单,每个订单的信息写在"订单小票"(HostOsScanStats)上,骑手送哪个订单就拿对应的小票


3. 全局静态变量:性能统计的核心

3.1 代码解析

cpp 复制代码
static struct scan_performance_vars perf;
关键字/标识符 含义
static 静态变量,限制变量的作用域 (只在当前文件可见),且生命周期贯穿整个程序运行期
struct scan_performance_vars 自定义的结构体类型,专门用来封装"扫描性能相关的所有数据"
perf 变量名(performance 的缩写),是这个结构体的实例

简单说:这行代码定义了一个只在当前文件可见、全程存在的"扫描性能统计账本"

3.2 结构体定义

cpp 复制代码
struct scan_performance_vars {
    // 速率相关
    int total_probes_sent;       // 累计发送的探测包总数
    int total_probes_recv;       // 累计接收的响应包总数
    float send_rate;             // 当前发送速率(包/秒)
    float recv_rate;             // 当前接收速率(包/秒)
    
    // 丢包/重试相关
    int lost_probes;             // 丢失的探测包数(发了没回应)
    int retry_count;             // 重试次数
    float loss_rate;             // 丢包率(lost/total_sent)
    
    // 耗时相关
    float avg_round_trip_time;   // 平均往返时延(RTT)
    int max_concurrent_probes;   // 最大并发探测数
    
    // 状态标记
    bool is_rate_limited;        // 是否触发了速率限制
    time_t last_update_time;     // 最后一次更新统计的时间
};

3.3 核心作用

3.3.1 统一收集"全量扫描"的性能数据(全局视角)
  • HostOsScanStats单主机的状态(比如"给192.168.1.1发了5个包")
  • perf整个扫描任务的全局统计(比如"总共扫了100台主机,发了1000个包,丢了20个,平均速率50包/秒")

生活化类比:

  • HostOsScanStats = 每个外卖订单的配送记录("订单A送了30分钟,距离2公里")
  • perf = 骑手一天的整体业绩("今天送了50单,总里程80公里,平均配送时间25分钟,超时2单")
3.3.2 static关键字的关键作用
  • 不加static:这个变量是"全局可见"的,其他文件如果也定义了同名变量会冲突,且容易被误修改
  • 加static
    • ✅ 作用域只在当前 .cpp 文件,其他文件看不到,避免命名冲突
    • ✅ 生命周期是"程序启动到退出",统计数据不会中途丢失

3.4 实际使用示例

cpp 复制代码
// 发送探测包时,更新统计
void send_tcp_probe(...) {
    // 发送逻辑...
    
    // 更新全局性能统计
    perf.total_probes_sent++;          // 发送数+1
    perf.send_rate = calc_send_rate(); // 重新计算发送速率
    perf.last_update_time = time(NULL);// 更新时间戳
}

// 接收响应时,更新统计
void processResp(...) {
    // 响应处理逻辑...
    
    perf.total_probes_recv++;          // 接收数+1
    perf.recv_rate = calc_recv_rate(); // 重新计算接收速率
    perf.loss_rate = (float)perf.lost_probes / perf.total_probes_sent; // 计算丢包率
}

// 提供读取统计的接口(给外部用,比如打印扫描报告)
struct scan_performance_vars get_scan_perf_stats() {
    return perf; // 返回当前的性能统计结果
}

4. 拥塞控制结构体:扫描速率的智能调节器

4.1 核心定位

scan_performance_vars 结构体不是单纯的"统计账本" ,而是 OS 扫描中拥塞控制(Congestion Control)的核心配置/状态结构体 ------ 本质是"扫描速率的智能调节器",用来控制探测包发送的快慢、并发量,避免网络拥塞或被目标主机拉黑。

4.2 拥塞窗口(cwnd)核心概念

拥塞窗口(cwnd):简单说就是"当前允许同时发出去但还没收到响应的探测包最大数量" ------ cwnd 越大,并发越高、扫描越快;cwnd 越小,并发越低、越保守。

4.3 逐字段解释

4.3.1 拥塞窗口(cwnd)的核心阈值(基础配置)
字段名 中文含义 通俗解释 + 扫描场景作用
low_cwnd 最小拥塞窗口 哪怕网络再差,也至少允许同时发这么多探测包(比如设为 2),保证扫描不会完全停摆
host_initial_cwnd 单主机初始拥塞窗口 对每台新主机开始扫描时,初始允许并发的探测包数(比如设为 5),相当于"给单主机的初始油门"
group_initial_cwnd 全局初始拥塞窗口 对所有扫描主机的总并发上限(比如设为 100),避免整体发太多包压垮网络
max_cwnd 最大拥塞窗口 无论网络多好,单主机/全局的 cwnd 都不能超过这个数(比如设为 20),防止过度并发
4.3.2 cwnd 的增长规则("踩油门"的节奏)
字段名 中文含义 通俗解释 + 扫描场景作用
slow_incr 慢启动模式增量 「慢启动」是扫描初期的模式,每收到一个响应,cwnd 就加这么多(比如设为 1)------ 初期慢慢提速,避免一开始就发太多
ca_incr 拥塞避免模式增量 当 cwnd 超过阈值后进入「拥塞避免」模式,每经过一个 RTT(往返时延),cwnd 加这么多(比如设为 0.5)------ 平稳提速,防止拥塞
cc_scale_max cwnd 增量的最大缩放因子 限制 cwnd 增长的"上限倍数"(比如设为 4),避免某些极端情况下 cwnd 疯长
initial_ssthresh 初始慢启动阈值 cwnd 超过这个值,就从「慢启动」切换到「拥塞避免」模式(比如设为 10)------ 相当于"提速到一定程度就换平稳模式"
4.3.3 cwnd 的下降规则("踩刹车"的节奏)
字段名 中文含义 通俗解释 + 扫描场景作用
group_drop_cwnd_divisor 全局丢包时 cwnd 除数 只要有任何一个包丢了,全局 cwnd 就除以这个数(比如设为 2)------ 全局降速,比如 cwnd=100 → 丢包后变成 50
group_drop_ssthresh_divisor 全局丢包时慢启动阈值除数 同时降低全局的慢启动阈值(比如设为 2),让后续更难进入"快提速"的慢启动模式,更保守
host_drop_ssthresh_divisor 单主机丢包时慢启动阈值除数 某台主机丢包时,单独降低这台主机的慢启动阈值(比如设为 2)------ 只针对有问题的主机降速,不影响其他主机

4.4 实际扫描示例

假设配置:host_initial_cwnd=5slow_incr=1initial_ssthresh=10ca_incr=0.5group_drop_cwnd_divisor=2

扫描流程:

  1. 开始扫描某台主机 → 初始 cwnd=5(host_initial_cwnd),进入「慢启动」模式
  2. 每收到1个响应 → cwnd +=1(slow_incr)→ 5→6→7→...→10
  3. cwnd 到 10(initial_ssthresh)→ 切换到「拥塞避免」模式
  4. 每经过1个 RTT → cwnd +=0.5(ca_incr)→ 10→10.5→11→...→max_cwnd(20)就不再涨
  5. 如果出现丢包 → 全局 cwnd = 全局 cwnd / 2(group_drop_cwnd_divisor)→ 比如从 100 降到 50,整体降速
  6. 丢包后再恢复 → 从 low_cwnd(2)重新慢慢涨,避免再次拥塞

5. cwnd翻倍机制:慢启动阶段的核心特征

5.1 核心结论

cwnd 只在「慢启动阶段」才会快速翻倍,进入「拥塞避免阶段」后就不再翻倍,而是缓慢增长。

5.2 慢启动阶段定义

慢启动是 TCP 拥塞控制(也是扫描工具借鉴的核心逻辑)的第一个阶段,核心特点是:

网络状态未知时,cwnd 以"指数级"增长(比如 1→2→4→8→16...),快速试探网络的承载能力,直到触发"慢启动阈值(ssthresh)"。

5.3 cwnd 翻倍的具体条件

5.3.1 标准 TCP 慢启动:每收到一个 ACK,cwnd 加 1 → 一轮 RTT 后翻倍

TCP 场景:

  • 初始 cwnd=1,发送 1 个报文段 → 收到 1 个 ACK(响应)→ cwnd 变成 2
  • 接着发送 2 个报文段 → 收到 2 个 ACK → cwnd 变成 4
  • 一轮 RTT(往返时延)内,发出去的包都收到响应,cwnd 就从 1→2→4→8... 指数级翻倍

扫描场景(适配探测包):

  • 初始 cwnd = host_initial_cwnd(比如设为 2)
  • 每收到 1 个探测包的响应 → cwnd += slow_incr(通常设为 1)
  • 一轮 RTT 内,所有发出去的探测包都收到响应 → cwnd 翻倍(比如 2→4→8...)
5.3.2 扫描工具的优化:按"响应数/时间"翻倍(更灵活)
触发条件 例子(结合结构体)
累计收到 N 个响应 N = 当前 cwnd 值 → 比如 cwnd=5,收到 5 个响应 → cwnd 翻倍到 10(slow_incr=1 时刚好触发)
一轮 RTT 无丢包 只要一轮 RTT 内发出去的探测包都收到响应,不管数量多少,直接翻倍(简化逻辑)
未触发速率限制 只要 perf.is_rate_limited 为 false(无拥塞),每秒钟检查一次,cwnd 翻倍(扫描特有的时间驱动逻辑)

5.4 代码实现逻辑

cpp 复制代码
// 单主机的拥塞窗口状态(存在 HostOsScanStats 里)
struct HostOsScanStats {
    int current_cwnd;       // 当前cwnd
    int ssthresh;           // 该主机的慢启动阈值(初始=perf.initial_ssthresh)
    int resp_received;      // 本轮已收到的响应数
};

// 收到一个探测包响应时,更新cwnd
void on_probe_response(HostOsScanStats *hss) {
    // 1. 只在慢启动阶段(current_cwnd < ssthresh)才会触发翻倍逻辑
    if (hss->current_cwnd < hss->ssthresh) {
        // 2. 每收到1个响应,cwnd += slow_incr(=1)
        hss->current_cwnd += perf.slow_incr;
        hss->resp_received++;
        
        // 3. 本轮响应数 == 当前cwnd → 触发翻倍(简化逻辑)
        if (hss->resp_received == hss->current_cwnd) {
            hss->current_cwnd *= 2; // cwnd翻倍
            hss->resp_received = 0; // 重置响应计数
            printf("cwnd 翻倍:%d → %d\n", hss->current_cwnd/2, hss->current_cwnd);
        }
        
        // 4. 不能超过max_cwnd(上限)
        if (hss->current_cwnd > perf.max_cwnd) {
            hss->current_cwnd = perf.max_cwnd;
        }
    } else {
        // 拥塞避免阶段:不再翻倍,每次只加 ca_incr(比如0.5)
        hss->current_cwnd += perf.ca_incr;
    }
}

5.5 cwnd 停止翻倍的条件

翻倍不是无限的,满足以下任一条件就会停止:

  1. 达到慢启动阈值current_cwnd >= ssthreshinitial_ssthresh)→ 切换到拥塞避免阶段,不再翻倍
  2. 达到最大cwndcurrent_cwnd >= max_cwnd → 无论如何都不涨了
  3. 出现丢包/拥塞 :只要有探测包丢包 → 触发 group_drop_cwnd_divisor/host_drop_ssthresh_divisor,cwnd 直接减半(比如8→4),重新从慢启动开始
  4. 触发速率限制is_rate_limited = true → 强制停止增长,甚至降低cwnd

6. 初始化方法:动态配置拥塞控制参数

6.1 核心前置知识

在看代码前,先搞懂两个关键变量:

  1. o:全局的 NmapOps 结构体实例,存储用户输入的扫描配置(比如 -T4 时序等级、--min-parallelism 最小并发、--max-parallelism 最大并发)
  2. o.timing_level:Nmap 的时序等级(-T0~-T5),数字越大扫描越激进(越快),是这段代码中最核心的控制变量

6.2 逐行拆解代码

cpp 复制代码
void scan_performance_vars::init() {
  /* TODO: 后续要优化这些值,至少要受 -T 时序参数影响 */
  // 1. 初始化最小拥塞窗口 low_cwnd
  low_cwnd = o.min_parallelism ? o.min_parallelism : 1;
  /* 逻辑:
     - 如果用户通过 --min-parallelism 设置了最小并发数 → low_cwnd 用这个值;
     - 否则默认 1(哪怕网络再差,至少允许1个并发探测包);
     - 作用:保证扫描不会完全停摆,是速率的"底线"。
   */

  // 2. 初始化最大拥塞窗口 max_cwnd
  max_cwnd = MAX(low_cwnd, o.max_parallelism ? o.max_parallelism : 300);
  /* 逻辑:
     - MAX() 是取最大值的宏;
     - 优先用用户设置的 --max-parallelism,否则默认 300;
     - 但必须 ≥ low_cwnd(比如用户设 min=5、max=3 → 最终 max_cwnd=5,避免矛盾);
     - 作用:限制并发的"上限",防止过度发包导致网络拥塞。
   */

  // 3. 初始化全局初始拥塞窗口 group_initial_cwnd
  group_initial_cwnd = box(low_cwnd, max_cwnd, 10);
  /* 逻辑:
     - box(a, b, c) 是 Nmap 自定义宏 → 把 c 限制在 [a, b] 区间内(即 max(a, min(b, c)));
     - 这里是把初始全局 cwnd 设为 10,但不能小于 low_cwnd、不能大于 max_cwnd;
     - 例子:如果 low_cwnd=5、max_cwnd=20 → group_initial_cwnd=10;
            如果 low_cwnd=15、max_cwnd=20 → group_initial_cwnd=15(不能小于low);
     - 作用:给所有主机的总并发设置一个"初始值",平衡保守和激进。
   */

  // 4. 初始化单主机初始拥塞窗口 host_initial_cwnd
  host_initial_cwnd = group_initial_cwnd;
  /* 逻辑:
     - 单主机的初始 cwnd 和全局初始值一致(简化设计);
     - 后续可根据单主机的丢包/响应情况单独调整,不影响全局;
     - 作用:单主机扫描的"初始油门",和全局保持一致避免局部过载。
   */

  // 5. 初始化慢启动增量 slow_incr
  slow_incr = 1;
  /* 逻辑:
     - 慢启动阶段,每收到1个响应,cwnd 加1(固定值);
     - 这是标准 TCP 慢启动的经典配置,保证指数级增长(翻倍)的基础;
     - 作用:控制慢启动的"提速步长"。
   */

  // 6. 初始化拥塞避免增量 ca_incr(受时序等级影响)
  /* 拥塞窗口在激进时序下增长更快 */
  if (o.timing_level < 4)
    ca_incr = 1;
  else
    ca_incr = 2;
  /* 逻辑:
     - o.timing_level(-T0~-T5):<4 是保守/普通模式(-T0~T3),≥4 是激进模式(-T4/T5);
     - 保守模式:拥塞避免阶段 cwnd 每次加1(平稳增长);
     - 激进模式:每次加2(更快增长,适配快速扫描需求);
     - 作用:时序越激进,拥塞避免阶段的提速越快。
   */

  // 7. 初始化 cwnd 增量最大缩放因子 cc_scale_max
  cc_scale_max = 50;
  /* 逻辑:
     - 限制 cwnd 增量的"最大倍数"(比如本来该加1,缩放后最多加50);
     - 防止极端情况下 cwnd 疯长(比如网络突然变通畅);
     - 作用:给增量加"天花板",避免失控。
   */

  // 8. 初始化初始慢启动阈值 initial_ssthresh
  initial_ssthresh = 75;
  /* 逻辑:
     - cwnd 超过75就从慢启动(翻倍增长)切换到拥塞避免(缓慢增长);
     - 固定值,后续可根据丢包动态调整;
     - 作用:慢启动和拥塞避免的"切换临界点"。
   */

  // 9. 初始化全局丢包时 cwnd 除数 group_drop_cwnd_divisor
  group_drop_cwnd_divisor = 2.0;
  /* 逻辑:
     - 只要有丢包,全局 cwnd 直接除以2(减半);
     - 标准 TCP 拥塞控制的"快速恢复"策略,快速降速避免进一步拥塞;
     - 作用:丢包时的"刹车力度"(全局)。
   */

  // 10. 初始化 ssthresh 除数(受时序等级影响)
  /* 根据时序等级调整 ssthresh 的下降幅度 */
  double ssthresh_divisor;
  if (o.timing_level <= 3)
    ssthresh_divisor = (3.0 / 2.0); // 1.5
  else if (o.timing_level <= 4)
    ssthresh_divisor = (4.0 / 3.0); // ~1.333
  else
    ssthresh_divisor = (5.0 / 4.0); // 1.25
  group_drop_ssthresh_divisor = ssthresh_divisor;
  host_drop_ssthresh_divisor = ssthresh_divisor;
  /* 逻辑:
     - 时序越保守(≤3),ssthresh 下降越多(除以1.5)→ 更谨慎,后续难进入慢启动;
     - 时序越激进(≥5),ssthresh 下降越少(除以1.25)→ 更大胆,快速恢复提速;
     - 例子:ssthresh=75,保守模式丢包后 → 75/1.5=50;激进模式 → 75/1.25=60;
     - 作用:丢包后调整"慢启动阈值",时序越激进,恢复越快。
   */
}

6.3 实际场景示例

假设用户执行扫描命令:nmap -T4 --min-parallelism=2 --max-parallelism=200 192.168.1.0/24

  • o.timing_level=4o.min_parallelism=2o.max_parallelism=200
  • 初始化后关键参数值:
参数名 计算过程 最终值
low_cwnd o.min_parallelism=2 2
max_cwnd MAX(2, 200) 200
group_initial_cwnd box(2, 200, 10) 10
host_initial_cwnd = group_initial_cwnd 10
slow_incr 固定1 1
ca_incr o.timing_level=4 → 2 2
initial_ssthresh 固定75 75
group_drop_cwnd_divisor 固定2.0 2.0
ssthresh_divisor o.timing_level=4 → 4/3 ~1.333

扫描时的表现:

  1. 初始全局 cwnd=10,单主机 cwnd=10
  2. 慢启动阶段:每收到1个响应,cwnd+1 → 快速翻倍到75
  3. 拥塞避免阶段:cwnd 每次+2(比保守模式快)
  4. 丢包时:全局 cwnd 减半(10→5),ssthresh 从75→56.25(75/1.333),恢复更快

7. TCP拥塞控制四大算法:网络传输的基石

7.1 慢启动 Slow Start

  • 刚建连时cwnd = 1 MSS
  • 规则每收到一个 ACK,cwnd += 1
  • 效果一个 RTT 后 cwnd 翻倍 ,呈指数增长
  • 直到cwnd >= ssthresh,进入拥塞避免

目的:慢慢试探网络,不一开始就打满。

7.2 拥塞避免 Congestion Avoidance

  • 规则每个 RTT,cwnd 只 +1
  • 效果线性缓慢增长
  • 一直增长到出现丢包/超时

加法增大 AI(Additive Increase)

7.3 快速重传 Fast Retransmit

  • 收到 3 个重复 ACK → 判定丢包
  • 不等超时,直接重传丢失的包
  • 比超时重传更快、更平滑

7.4 快速恢复 Fast Recovery(配合快速重传)

收到 3 个重复 ACK 时:

  1. ssthresh = cwnd / 2
  2. cwnd = cwnd / 2
  3. 直接进入拥塞避免,不再回到 cwnd=1

乘法减小 MD(Multiplicative Decrease)

7.5 超时重传(最严厉)

一旦定时器超时

  1. ssthresh = cwnd / 2
  2. cwnd = 1
  3. 重新走慢启动

网络拥塞严重,直接"跪到底"。

7.6 一句话串起整个流程

  1. 刚开始:慢启动(指数涨)
  2. 过阈值:拥塞避免(线性涨)
  3. 丢包但收到 3 个重复 ACK:快速重传 + 快速恢复
  4. 直接超时:窗口砍半 + 重置为1,重走慢启动

7.7 最经典口诀

慢启动翻倍,拥塞避免加一
三次ACK快重传,快速恢复不回一
一旦超时全重来,窗口直接降到一


8. MSS概念:TCP报文的最大数据长度

8.1 一句话定义

MSS = Maximum Segment Size

= 一个 TCP 段里,纯数据部分的最大字节数。

公式:

复制代码
MSS ≈ MTU - IP头(20) - TCP头(20)

日常以太网里:

  • MTU = 1500
  • MSS ≈ 1460 字节

8.2 用最通俗的比喻

  • 一辆货车:整个 IP 包
  • 车头:IP 头 + TCP 头
  • 车厢里的货:TCP 数据

MSS = 车厢最多能装多少货

8.3 和拥塞控制的关系

在 TCP 拥塞控制里:

  • cwnd(拥塞窗口)的单位经常就是 多少个 MSS
  • 比如:
    • cwnd = 1 → 一次最多发 1 个 MSS 的数据
    • cwnd = 8 → 一次最多发 8 个 MSS 的数据

所以文章里说:

慢启动 cwnd 从 1 开始指数增长

这里的 1,就是 1 个 MSS

8.4 简单记忆

MSS = TCP 一次能发的"最大纯数据长度"
cwnd = 一次能发几个 MSS


9. 时间计算函数:扫描耗时的精确计量

9.1 核心作用

这个函数的核心功能是:计算从 Nmap 扫描启动到"当前时刻"的耗时,返回以"秒"为单位的浮点值(比如 10.5 秒、0.23 秒),是扫描过程中"计时、速率控制、超时判断"的基础工具函数。

9.2 代码实现

cpp 复制代码
// ===================== 计算扫描启动后的耗时(秒) =====================
// now: 可选参数,传入当前时间(避免重复调用gettimeofday)
float NmapOps::TimeSinceStart(const struct timeval *now) {
    struct timeval tv;
    if (!now)  // 未传入当前时间,则获取系统当前时间
        gettimeofday(&tv, NULL);
    else tv = *now;

    // 计算当前时间与启动时间的差值(浮点秒数)
    return TIMEVAL_FSEC_SUBTRACT(tv, start_time);
}

9.3 关键数据类型

标识符/类型 含义
struct timeval Linux/Unix 系统的"高精度时间结构体",存储秒+微秒,精度达 1 微秒(1e-6 秒)
start_time NmapOps 类的成员变量,记录扫描启动时的时间
now 可选入参:如果外部已经获取过当前时间,直接传进来复用,避免重复系统调用
gettimeofday(&tv, NULL) 系统调用:获取当前系统的高精度时间

9.4 核心宏 TIMEVAL_FSEC_SUBTRACT

cpp 复制代码
// 自定义宏:计算两个 timeval 的差值(t1 - t0),返回浮点秒数
#define TIMEVAL_FSEC_SUBTRACT(t1, t0) \
    ((t1.tv_sec - t0.tv_sec) + (t1.tv_usec - t0.tv_usec) / 1000000.0)

拆解计算逻辑:

  1. t1.tv_sec - t0.tv_sec:计算"秒数差值"
  2. (t1.tv_usec - t0.tv_usec) / 1000000.0:计算"微秒差值"并转换成秒
  3. 两者相加:得到高精度浮点秒数

9.5 实际使用场景

场景1:打印扫描耗时
cpp 复制代码
float elapsed = o.TimeSinceStart(NULL);
printf("扫描耗时:%.2f 秒\n", elapsed); // 输出:扫描耗时:10.50 秒
场景2:速率控制
cpp 复制代码
float elapsed = o.TimeSinceStart(&current_tv);
int sent_count = 150;
float send_rate = sent_count / elapsed;
if (send_rate > 100) {
    usleep(10000); // 速率超限,休眠10毫秒
}
场景3:超时判断
cpp 复制代码
float host_elapsed = o.TimeSinceStart(NULL);
if (host_elapsed > 30.0) {
    printf("主机扫描超时(耗时%.2f秒),放弃\n", host_elapsed);
    return;
}

9.6 关键设计细节

  1. 可选参数 now 的价值 :避免重复调用 gettimeofday(系统调用比普通函数慢)
  2. 浮点秒数的优势:保留了微秒精度,适合高精度的速率控制、超时判断
  3. 鲁棒性 :即使 nowNULL,也能自动兜底获取当前时间

10. 抓包器初始化:构建高性能抓包环境

10.1 核心目标

这个函数的唯一目的:

HostOsScan(单主机 OS 扫描引擎)初始化高性能的抓包器 ------ 打开指定网卡的 pcap 抓包句柄,并设置精准的 BPF 过滤器,只抓取"和当前 OS 扫描相关的数据包"。

10.2 关键前置知识

  1. pcap:Linux/Unix 下的抓包库(WinPcap/Npcap 是Windows版本)
  2. BPF 过滤器:Berkeley Packet Filter,是内核态的数据包过滤规则,只把符合规则的包交给用户程序,能大幅减少用户态处理的数据包数量

10.3 完整执行流程

阶段1:初始化变量 + 决定过滤策略
cpp 复制代码
char pcap_filter[2048]; // 存完整BPF过滤规则
char dst_hosts[1200];   // 存"源地址列表"子串
int filterlen = 0, len;
unsigned int targetno;
// 核心判断:目标数≤20时,用"精确地址过滤";否则用"模糊过滤"
bool doIndividual = Targets.size() <= 20;
pcap_filter[0] = '\0';
阶段2:构建源地址过滤子串(仅目标数≤20时)
cpp 复制代码
if (doIndividual) {
  for (targetno = 0; targetno < Targets.size(); targetno++) {
    len = Snprintf(dst_hosts + filterlen, sizeof(dst_hosts) - filterlen,
                   "%ssrc host %s", (targetno==0)?"":" or ", 
                   Targets[targetno]->targetipstr());
    if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
      fatal("ran out of space in dst_hosts");
    filterlen += len;
  }
  len = Snprintf(dst_hosts + filterlen, sizeof(dst_hosts) - filterlen, ")))");
  if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
    fatal("ran out of space in dst_hosts");
}

例子 :若目标是 192.168.1.1、192.168.1.2,dst_hosts 最终会是:
src host 192.168.1.1 or src host 192.168.1.2)))

阶段3:打开pcap抓包句柄
cpp 复制代码
HOS->pd = my_pcap_open_live(
  Targets[0]->deviceName(), // 网卡名(如eth0)
  8192,                    // snaplen:只抓8KB包头
  o.spoofsource ? 1 : 0,   // 混杂模式
  pcap_selectable_fd_valid() ? 200 : 2 // 超时时间
);
if (HOS->pd == NULL)
  fatal("%s", PCAP_OPEN_ERRMSG);
阶段4:构建完整BPF过滤器 + 生效
cpp 复制代码
struct sockaddr_storage ss = Targets[0]->source();
if (ss.ss_family == AF_INET) {
  if (doIndividual) { // 目标少:精确过滤
    len = Snprintf(pcap_filter, sizeof(pcap_filter),
                   "dst host %s and (icmp or (tcp and (%s",
                   inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr), dst_hosts);
  } else { // 目标多:模糊过滤
    len = Snprintf(pcap_filter, sizeof(pcap_filter),
                   "dst host %s and (icmp or tcp)",
                   inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr));
  }
  if (len < 0 || len >= (int) sizeof(pcap_filter))
    fatal("ran out of space in pcap filter");

  if (o.debugging)
    log_write(LOG_PLAIN, "Packet capture filter (device %s): %s\n",
              Targets[0]->deviceFullName(), pcap_filter);
  set_pcap_filter(Targets[0]->deviceFullName(), HOS->pd, pcap_filter);
}

完整过滤器例子 (目标少+本机IP=192.168.1.100):
dst host 192.168.1.100 and (icmp or (tcp and (src host 192.168.1.1 or src host 192.168.1.2))))

过滤器含义:只抓取"目的地址是本机、协议是ICMP,或协议是TCP且源地址是扫描目标"的包

10.4 核心设计亮点

  1. 性能优先

    • 内核态BPF过滤:减少用户态处理的数据包数量
    • 分策略过滤:目标少则精确过滤,目标多则模糊过滤
    • 只抓包头(snaplen=8192):节省内存和CPU
  2. 安全可靠

    • 所有字符串拼接用 Snprintf + 越界检查
    • 抓包句柄打开失败直接终止
    • 括号配对严格检查
  3. 兼容性/鲁棒性

    • 仅处理IPv4,IPv6分支留空
    • 调试模式打印过滤器
    • 依赖封装的 my_pcap_open_live

11. 字符串安全拼接:Snprintf详解

11.1 核心作用

这行代码的唯一目的是:把当前遍历到的目标IP,以"src host IP"的格式(首个IP不加前缀,后续IP加" or "前缀),安全地追加到 dst_hosts 缓冲区的指定位置

11.2 完整代码

cpp 复制代码
len = Snprintf(
  dst_hosts + filterlen, // 参数1:写入的起始地址
  sizeof(dst_hosts) - filterlen, // 参数2:最大可写入的字符数
  "%ssrc host %s", // 参数3:格式化模板
  (targetno == 0)? "" : " or ", // 参数4:第一个占位符的取值(前缀)
  Targets[targetno]->targetipstr() // 参数5:第二个占位符的取值(目标IP)
);

11.3 逐个参数拆解

参数1:dst_hosts + filterlen ------ 决定"从缓冲区的哪个位置开始写"
  • 本质:这是一个指针偏移操作
  • 第一次循环(targetno=0):filterlen=0dst_hosts + 0 = 缓冲区起始地址
  • 第二次循环(targetno=1):假设第一次写入了16个字符,filterlen 已更新为16 → dst_hosts + 16 = 缓冲区第17个字符的位置
  • 核心意义:实现"追加写入",而不是每次都从头覆盖
参数2:sizeof(dst_hosts) - filterlen ------ 决定"最多能写多少字符"
  • 本质:计算缓冲区的剩余可用空间
  • Snprintf 的关键特性:会严格遵守这个数值,最多只写「该数值-1」个字符(留1个位置给 \0
  • 核心意义:从根源避免"缓冲区溢出"
参数3:"%ssrc host %s" ------ 决定"写入的内容格式"
  • 本质:这是格式化模板字符串 ,里面的 %s 是"占位符"
  • 模板拆解:
    • 第一个 %s:替换成"前缀"(要么是空字符串,要么是" or ")
    • src host :固定字符串(BPF过滤器的语法)
    • 第二个 %s:替换成目标IP字符串
  • 核心意义:统一所有目标IP的格式,符合BPF过滤器的语法要求
参数4:(targetno == 0)? "" : " or " ------ 决定"是否加'or'前缀"
  • 本质:这是三元运算符
  • 第一次循环(targetno=0):前缀是空字符串 → 直接写"src host 192.168.1.1"
  • 第二次循环(targetno=1):前缀是" or " → 写" or src host 192.168.1.2"
  • 核心意义:保证拼接后的字符串语法正确
参数5:Targets[targetno]->targetipstr() ------ 提供"要拼接的目标IP字符串"
  • 本质:调用 Target 类的成员函数 targetipstr(),返回当前遍历到的目标主机的IP地址字符串
  • 核心意义:动态获取每个目标的IP,不用硬编码

11.4 核心设计亮点

  1. 安全优先 :用 Snprintf + 剩余空间计算,彻底杜绝缓冲区溢出
  2. 动态追加:通过指针偏移和已用长度记录,实现多个IP的无缝拼接
  3. 语法合规:三元运算符控制"or"前缀,保证BPF过滤器能识别
  4. 灵活性:适配任意数量的目标(≤20)

12. 跨平台接口名转换:Windows平台的特殊处理

12.1 函数整体说明

c 复制代码
int DnetName2PcapName(const char *dnetdev, char *pcapdev, int pcapdevlen)

功能:

  • 输入:dnetdev(dnet 库使用的网卡名)
  • 输出:pcapdev(对应的 pcap 格式设备名)
  • 返回值:
    • 1:成功找到并写入 pcapdev
    • 0:找不到对应的 pcap 设备名

此转换 仅在 Windows 上需要,因为 Windows 下 dnet 和 pcap 使用的接口命名格式不同。

12.2 静态缓存结构

c 复制代码
static struct NameCorrelationCache {
  char dnetd[64];
  char pcapd[128];
} *NCC = NULL;
static int NCCsz = 0;
static int NCCcapacity = 0;
  • NCC:已成功转换的映射缓存(dnet → pcap)
  • NCCsz:当前缓存数量
  • NCCcapacity:当前缓存容量
c 复制代码
static struct NameNotFoundCache {
  char dnetd[64];
} *NNFC = NULL;
static int NNFCsz = 0;
static int NNFCcapacity = 0;
  • NNFC:记录 查过但找不到对应 pcap 名称 的 dnet 设备名,避免重复查询

12.3 初始化缓存

c 复制代码
if (!NCC) {
  NCCcapacity = 5;
  NCC = (struct NameCorrelationCache *) safe_zalloc(NCCcapacity * sizeof(*NCC));
  NCCsz = 0;
}
if (!NNFC) {
  NNFCcapacity = 5;
  NNFC = (struct NameNotFoundCache *) safe_zalloc(NNFCcapacity * sizeof(*NNFC));
  NNFCsz = 0;
}
  • 第一次调用时分配初始缓存空间(5 条)
  • 使用 safe_zalloc,表示分配并清零,防止脏数据

12.4 查询缓存

第一步:查成功缓存
c 复制代码
for (i = 0; i < NCCsz; i++) {
  if (strcmp(NCC[i].dnetd, dnetdev) == 0) {
    Strncpy(pcapdev, NCC[i].pcapd, pcapdevlen);
    return 1;
  }
}

如果该 dnetdev 已经转换过,直接从缓存取值,避免再次调用系统接口。

第二步:查失败缓存
c 复制代码
for (i = 0; i < NNFCsz; i++) {
  if (strcmp(NNFC[i].dnetd, dnetdev) == 0) {
    return 0;
  }
}

如果之前已经确认这个设备名 找不到对应的 pcap 名称,直接返回失败。

12.5 调用系统接口转换

c 复制代码
if (intf_get_pcap_devname(dnetdev, tmpdev, sizeof(tmpdev)) != 0) {
    // 转换失败:将该dnet名称加入"未找到"缓存
    if (NNFCsz >= NNFCcapacity) {
      NNFCcapacity <<= 2;  // 容量乘 4
      NNFC = (struct NameNotFoundCache *) safe_realloc(NNFC,
                                                       NNFCcapacity * sizeof(*NNFC));
    }
    Strncpy(NNFC[NNFCsz].dnetd, dnetdev, sizeof(NNFC[0].dnetd));
    NNFCsz++;
    return 0;
}

12.6 转换成功处理

c 复制代码
if (NCCsz >= NCCcapacity) {
  NCCcapacity <<= 2;
  NCC = (struct NameCorrelationCache *) safe_realloc(NCC,
                                                     NCCcapacity * sizeof(*NCC));
}
Strncpy(NCC[NCCsz].dnetd, dnetdev, sizeof(NCC[0].dnetd));
Strncpy(NCC[NCCsz].pcapd, tmpdev, sizeof(NCC[0].pcapd));
NCCsz++;
Strncpy(pcapdev, tmpdev, pcapdevlen);
return 1;

12.7 总结

这段代码实现了:

在 Windows 下,将 dnet 接口名转换为 pcap 接口名,并通过双缓存(成功缓存 + 失败缓存)机制提高性能,避免重复查询系统接口。


13. 随机数生成:加密级安全随机源

13.1 get_random_u32() 函数

cpp 复制代码
u32 get_random_u32() {
  u32 i;
  get_random_bytes(&i, sizeof(i));
  return i;
}

核心作用:

  • 通过系统安全随机源生成高质量的32位无符号随机数
  • 依赖 get_random_bytes() 接口,保证了随机数的不可预测性
  • 函数返回的随机数直接用于设置 TCP 序列号、ACK 值等核心扫描参数

13.2 nrand_init() 函数

13.2.1 数据结构
cpp 复制代码
struct nrand_handle {
  u8    i, j;          // RC4 算法的两个索引变量(核心状态)
  u8    s[256];        // RC4 的 S 盒(256字节置换表,算法核心)
  u8    *tmp;          // 临时缓存指针
  int   tmplen;        // 临时缓存长度
};
13.2.2 完整实现
cpp 复制代码
void nrand_init(nrand_h *r) {
  u8 seed[256];
  int i;

  /* 第一步:跨平台收集高熵种子 */
#ifdef WIN32
  // Windows 平台:使用系统加密API获取安全随机种子
  HCRYPTPROV hcrypt = 0;
  CryptAcquireContext(&hcrypt, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
  CryptGenRandom(hcrypt, sizeof(seed), seed);
  CryptReleaseContext(hcrypt, 0);
#else
  // Linux/Unix 平台:混合多源熵值 + 读取系统随机设备
  struct timeval *tv = (struct timeval *)seed;
  int *pid = (int *)(seed + sizeof(*tv));
  int fd;

  gettimeofday(tv, NULL);
  *pid = getpid();

  if ((fd = open("/dev/urandom", O_RDONLY)) != -1 ||
      (fd = open("/dev/arandom", O_RDONLY)) != -1) {
    ssize_t n;
    do {
      errno = 0;
      n = read(fd, seed + sizeof(*tv) + sizeof(*pid),
               sizeof(seed) - sizeof(*tv) - sizeof(*pid));
    } while (n < 0 && errno == EINTR);
    close(fd);
  }
#endif

  /* 第二步:初始化 RC4 的 S 盒 */
  for (i = 0; i < 256; i++) { r->s[i] = i; };
  r->i = r->j = 0;

  /* 第三步:用种子打乱 S 盒(密钥调度算法 KSA) */
  nrand_addrandom(r, seed, 128);
  nrand_addrandom(r, seed + 128, 128);

  /* 第四步:初始化临时缓存 */
  r->tmp = NULL;
  r->tmplen = 0;

  /* 第五步:丢弃前 1KB 随机数据(消除 RC4 初始偏置) */
  nrand_get(r, seed, 256); nrand_get(r, seed, 256);
  nrand_get(r, seed, 256); nrand_get(r, seed, 256);
}

13.3 HCRYPTPROV 类型

HCRYPTPROV 本质上是 Windows CryptoAPI 中的一个句柄类型,你可以把它理解成:

  • 一个"加密服务上下文的身份证",用来唯一标识一个打开的、与加密服务提供程序(CSP) 的连接
  • 类比生活中的例子:你去银行办理业务,银行会给你一个排号单(句柄),凭这个排号单才能调用银行的服务

从代码层面,它的底层定义在 wincrypt.h 中:

cpp 复制代码
typedef unsigned long HCRYPTPROV;
#define NULL_PROV_HANDLE ((HCRYPTPROV)0)

13.4 CryptAcquireContext 函数

cpp 复制代码
CryptAcquireContext(&hcrypt, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);

逐参数拆解:

参数 含义 作用
&hcrypt 输出:加密上下文句柄 函数执行成功后,会把有效的加密上下文句柄写入 hcrypt 变量中
NULL 密钥容器名称 指定"密钥容器"的名称,传NULL表示使用默认容器
NULL CSP名称 指定要使用的加密服务提供程序(CSP)的名称,传NULL表示使用系统默认的CSP
PROV_RSA_FULL CSP类型 指定要使用的CSP类型,支持RSA加密/解密、数字签名/验证、哈希计算,同时也支持生成安全随机数
CRYPT_VERIFYCONTEXT 操作标志 无需用户权限、无需访问密钥容器、无需交互式登录(比如服务/后台程序也能调用)

13.5 跨平台熵源设计思路

平台 熵源组成 安全性
Windows 系统加密API(CryptGenRandom) 最高(底层混合硬件/系统事件熵值)
Linux 时间戳 + 进程ID + /dev/urandom 高(/dev/urandom 是系统熵池的非阻塞接口)

13.6 为什么要丢弃前 1KB 数据?

RC4 算法存在初始输出偏置问题:前几百字节的随机数分布不够均匀,容易被破解。

通过调用 4 次 nrand_get 生成 1024 字节并丢弃,让 RC4 算法运行到稳定状态,后续生成的随机数才符合加密安全要求。

13.7 与上层函数的完整链路

复制代码
HostOsScan::reInitScanSystem()
  ↓
get_random_u32() → get_random_bytes()
  ↓
nrand_get()(基于 RC4 PRGA 生成随机字节)
  ↓
nrand_init()(初始化 RC4 状态,保证 nrand_get 有安全的随机源)

总结

本文档深入解析了Nmap OS扫描的核心技术,涵盖了从循环迭代器设计到随机数生成的完整技术栈。主要知识点包括:

  1. 循环迭代器:实现高效遍历未完成主机列表的机制
  2. 执行与状态分离:工业级设计模式,支持实例复用和并发安全
  3. 全局静态变量:性能统计的核心,统一收集全量扫描数据
  4. 拥塞控制结构体:扫描速率的智能调节器,动态调节探测包并发量
  5. cwnd翻倍机制:慢启动阶段的核心特征,实现指数级增长
  6. 初始化方法:动态配置拥塞控制参数,适配用户需求
  7. TCP拥塞控制四大算法:网络传输的基石,保证网络稳定性
  8. MSS概念:TCP报文的最大数据长度,拥塞控制的基础单位
  9. 时间计算函数:扫描耗时的精确计量,支持速率控制和超时判断
  10. 抓包器初始化:构建高性能抓包环境,通过BPF过滤器精准过滤
  11. 字符串安全拼接:Snprintf详解,避免缓冲区溢出
  12. 跨平台接口名转换:Windows平台的特殊处理,通过缓存机制提升性能
  13. 随机数生成:加密级安全随机源,基于RC4算法实现

这些技术共同构成了Nmap OS扫描的核心能力,使其成为工业级网络扫描工具的典范。通过深入理解这些技术,开发者可以更好地掌握网络扫描的核心原理,并将其应用到实际项目中。

相关推荐
x***r15112 小时前
SuperScan4单文件扫描安装步骤详解(附端口扫描与主机存活检测教程)
windows
不爱学习的老登13 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
陌陌龙15 小时前
全免去水印大师 v1.7.6 | 安卓端高效水印处理神器
windows
csdn2015_16 小时前
将object转换成list
开发语言·windows·python
SJjiemo19 小时前
LeafView 图片查看器
windows
ENEN988120 小时前
【精品珍藏自购付费资源】2026年日历PSD模板合集【PSD CDR多格式可编辑】已分类含预览 [7.5G]
windows·经验分享·电脑
njsgcs20 小时前
windows ui窗口post 我wsl开放的flask llm端点
windows·ui·flask·post
这波不该贪内存的21 小时前
数据结构三个典型的类型整理
数据结构·windows
love530love1 天前
突破 Windows 编译禁区:BitNet 1-bit LLM 推理框架 GPU 加速部署编译 BitNet CUDA 算子全记录
c++·人工智能·pytorch·windows·python·cuda·bitnet
咚咚王者1 天前
人工智能之视觉领域 计算机视觉 第二章 环境搭建(Windows/Mac/Linux通用)
人工智能·windows·计算机视觉