sprintf & snprintf

目录

一、基础核心

[1. 函数原型与核心参数(Linux glibc 标准)](#1. 函数原型与核心参数(Linux glibc 标准))

[2. 核心差异对比表](#2. 核心差异对比表)

[3. 基础用法示例(Linux 环境,对比直观)](#3. 基础用法示例(Linux 环境,对比直观))

二、补充知识点

[1. sprintf 与 snprintf 的核心区别?](#1. sprintf 与 snprintf 的核心区别?)

[2. snprintf 的返回值为什么设计成 "应写入总字节数"?](#2. snprintf 的返回值为什么设计成 “应写入总字节数”?)

[3. Linux 下 sprintf 缓冲区溢出的具体后果?](#3. Linux 下 sprintf 缓冲区溢出的具体后果?)

[4. snprintf 一定安全吗?有哪些潜在陷阱?](#4. snprintf 一定安全吗?有哪些潜在陷阱?)

[5. Linux 下如何避免格式化字符串漏洞?](#5. Linux 下如何避免格式化字符串漏洞?)

[三、Linux 实战](#三、Linux 实战)

[1. Linux 环境规范](#1. Linux 环境规范)

[2. 实战 1:日志输出场景](#2. 实战 1:日志输出场景)

[3. 实战 2:协议封装场景(网络开发高频)](#3. 实战 2:协议封装场景(网络开发高频))

四、总结


一、基础核心

1. 函数原型与核心参数(Linux glibc 标准)

二者都属于stdio.h库,核心功能是将格式化数据写入字符串缓冲区,差异集中在缓冲区控制返回值设计

sprintf(危险版,无边界控制)

cpp 复制代码
#include <stdio.h>
// 功能:将格式化内容写入str缓冲区
// 参数:str-目标缓冲区;format-格式化模板;...-可变参数(与模板占位符对应)
// 返回值:成功-写入字节数(不含\0);失败-负数
int sprintf(char *str, const char *format, ...);

致命缺陷 :不检查str的缓冲区大小,若格式化后内容长度超过缓冲区容量,直接触发缓冲区溢出

snprintf(安全版,带边界控制)

cpp 复制代码
#include <stdio.h>
// 新增参数size:缓冲区最大可写入字节数(含末尾的\0终止符)
int snprintf(char *str, size_t size, const char *format, ...);

核心优势 :强制边界控制,无论内容多长,最多写入size-1个有效字符,末尾自动补 \0,从根源避免溢出。

2. 核心差异对比表
对比维度 sprintf snprintf
边界控制 无,溢出风险极高 有,按 size 截断,自动补 \0
安全性 高危,易引发 Linux 系统漏洞 安全,工业级代码首选
返回值含义 成功 = 实际写入字节数(不含 \0);失败 = 负数 成功 = 未截断返回实际写入数,截断返回应写入总字节数;失败 = 负数
兼容性 C89 标准,全 Linux 版本兼容 C99 标准,glibc 2.1 + 主流系统均支持
适用场景 仅长度 100% 确定的极端场景(几乎不用) 日志、协议、配置等所有格式化场景
3. 基础用法示例(Linux 环境,对比直观)

示例 1:sprintf 反面教材(缓冲区溢出风险)

cpp 复制代码
#include <stdio.h>

int main() {
    char buf[10];  // 栈缓冲区,仅10字节(9个有效字符+1个\0)
    // 格式化内容"Value: 123456"共11字节(不含\0),超过缓冲区大小
    sprintf(buf, "Value: %d", 123456);
    // 后果:可能输出乱码、触发段错误(SIGSEGV),甚至被黑客利用
    printf("Result: %s\n", buf);
    return 0;
}

Linux 下溢出危害:栈缓冲区溢出会覆盖相邻变量、函数返回地址,轻则程序崩溃,重则被注入恶意代码实现提权。

示例 2:snprintf 正面教材(安全截断 + 返回值判断)

cpp 复制代码
#include <stdio.h>

int main() {
    char buf[10];  // 10字节缓冲区
    // 格式化内容11字节,按size=10截断,写入9个有效字符+1个\0
    int ret = snprintf(buf, sizeof(buf), "Value: %d", 1234);

    printf("返回值:%d | 格式化结果:%s\n", ret, buf);
    // 输出:返回值:11 | 格式化结果:Value: 123

    // 工业级必做:通过返回值判断是否截断
    if (ret < 0) {
        perror("snprintf failed");  // Linux标准错误打印
        return 1;
    } else if (ret >= sizeof(buf)) {
        fprintf(stderr, "警告:内容截断,需扩容至至少%d字节(含\\0)\n", ret + 1);
    }
    return 0;
}

关键技巧

  1. sizeof(buf)代替硬编码,减少维护成本;
  2. 返回值ret=11格式化后应有的总字节数 ,通过ret >= sizeof(buf)可精准判断截断;
  3. 无论是否截断,snprintf都会补\0,避免字符串无终止符导致的乱码。

二、补充知识点

1. sprintf 与 snprintf 的核心区别?
  • 边界控制 :sprintf 无检查,超缓冲区直接溢出;snprintf 通过size参数控制,最多写size-1字节,自动补\0
  • 安全性:sprintf 是 Linux 缓冲区溢出漏洞的主要源头,高危;snprintf 是安全标配;
  • 返回值:sprintf 仅返回实际写入数;snprintf 截断时返回 "应写入总字节数",支持动态扩容。
2. snprintf 的返回值为什么设计成 "应写入总字节数"?

为了动态适配变长内容 。比如处理用户输入、动态日志等长度不确定的场景时,可先调用snprintf(NULL, 0, ...)获取所需缓冲区大小,再动态分配内存,确保内容完整无截断。

示例代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int num = 12345678;
    char *buf = NULL;

    // 步骤1:传size=0,仅获取格式化后总长度(不写入缓冲区)
    int ret = snprintf(NULL, 0, "Value: %d", num);
    if (ret < 0) {
        perror("snprintf get length failed");
        return 1;
    }

    // 步骤2:动态分配内存(+1预留\0位置)
    buf = (char *)malloc(ret + 1);
    if (buf == NULL) {
        perror("malloc failed");
        return 1;
    }

    // 步骤3:完整写入内容
    snprintf(buf, ret + 1, "Value: %d", num);
    printf("完整内容:%s\n", buf);  // 输出:Value: 12345678

    free(buf);  // 工业级必做:释放堆内存,避免泄漏
    return 0;
}
3. Linux 下 sprintf 缓冲区溢出的具体后果?
  • 程序崩溃:溢出覆盖栈上函数返回地址,触发段错误(SIGSEGV),生成核心转储文件;
  • 逻辑异常:溢出覆盖相邻变量,导致数据错乱、程序行为异常(隐蔽性强,排查难度大);
  • 安全漏洞:黑客利用溢出注入 shellcode,修改程序执行流程,实现提权、篡改数据(历史上 Apache、Nginx 早期版本均出现过类似漏洞)。
4. snprintf 一定安全吗?有哪些潜在陷阱?

并非绝对安全,需规避 3 个核心陷阱:

  • size=0 陷阱 :若size=0str可传NULL,返回值仍为应写入总字节数,但无任何内容写入;
  • str=NULL 陷阱 :若size>0str=NULL,直接触发段错误(Linux 下 SIGSEGV);
  • 格式化符不匹配陷阱 :可变参数类型 / 数量与format不匹配(如%d对应字符串),会导致内存错乱,snprintf 无法规避。
5. Linux 下如何避免格式化字符串漏洞?
  • 禁用 sprintf:全量替换为 snprintf,强制边界控制;
  • 严格校验格式化符 :禁止使用%n(将已写入字节数写入变量,易被篡改),禁止用户输入作为format参数;
  • 动态扩容 :变长内容先通过snprintf(NULL,0,...)获取长度,再分配内存;
  • 工具检测 :编译时加-Wformat-security告警漏洞,用valgrind --tool=memcheck检测内存越界。

三、Linux 实战

1. Linux 环境规范
  • 环境:Ubuntu/CentOS,GCC 7.5+;
  • 编译选项:g++ -std=c++17 -O2 -Wall -Wformat-security -g-Wformat-security强制检查格式化安全);
  • 工业级规范:① 禁用 sprintf;② 所有 snprintf 必须校验返回值;③ 堆内存手动释放或用 RAII 封装;④ 避免硬编码缓冲区大小。
2. 实战 1:日志输出场景

日志需格式化时间、级别、内容,长度不确定,需安全实现动态扩容。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

// 工业级安全日志函数
void safe_log(const char *level, const char *msg) {
    if (level == NULL || msg == NULL) return;  // 空指针校验,避坑

    // 步骤1:格式化时间(固定长度,直接栈分配)
    char time_buf[32];
    time_t now = time(NULL);
    struct tm *tm_now = localtime(&now);
    snprintf(time_buf, sizeof(time_buf), "%04d-%02d-%02d %02d:%02d:%02d",
             tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday,
             tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec);

    // 步骤2:获取日志总长度,动态扩容
    int total_len = snprintf(NULL, 0, "[%s] [%s] %s\n", time_buf, level, msg);
    if (total_len < 0) {
        perror("snprintf get log len failed");
        return;
    }

    // 步骤3:分配堆内存
    char *log_buf = (char *)malloc(total_len + 1);
    if (log_buf == NULL) {
        perror("malloc log buf failed");
        return;
    }

    // 步骤4:完整写入日志
    snprintf(log_buf, total_len + 1, "[%s] [%s] %s\n", time_buf, level, msg);
    fprintf(stdout, "%s", log_buf);

    free(log_buf);  // 释放内存
}

int main() {
    safe_log("INFO", "程序启动成功,初始化完成");
    safe_log("ERROR", "数据库连接失败,重试中...");
    return 0;
}

核心亮点

  • 空指针校验:避免%s接收NULL导致崩溃;
  • 分阶段处理:固定长度内容(时间)用栈,变长内容(日志)用堆;
  • 严格错误处理:每步校验返回值,符合 Linux 工业级代码健壮性要求。
3. 实战 2:协议封装场景(网络开发高频)

Linux 网络开发中,自定义协议需严格控制长度,避免解析错误。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

// 自定义协议结构:头部(8字节) + 柔性数组数据
typedef struct {
    uint16_t cmd;   // 命令码
    uint16_t len;   // 数据长度
    uint32_t seq;   // 序列号
    char data[0];   // 柔性数组,存储数据
} ProtoHeader;

// 安全封装协议包
char *proto_pack(uint16_t cmd, uint32_t seq, const char *data, uint16_t data_len, uint32_t *pack_len) {
    if (data == NULL || data_len == 0 || pack_len == NULL) return NULL;

    // 计算协议包总长度
    *pack_len = sizeof(ProtoHeader) + data_len;
    ProtoHeader *pack = (ProtoHeader *)malloc(*pack_len);
    if (pack == NULL) {
        perror("malloc proto failed");
        return NULL;
    }

    // 填充头部(网络字节序转换,跨平台必备)
    pack->cmd = htons(cmd);
    pack->len = htons(data_len);
    pack->seq = htonl(seq);

    // 用snprintf复制数据,比memcpy更安全(自动补\0)
    snprintf(pack->data, data_len + 1, "%s", data);

    return (char *)pack;
}

int main() {
    const char *data = "Hello Linux Proto";
    uint32_t pack_len = 0;
    char *pack = proto_pack(0x0001, 12345, data, strlen(data), &pack_len);
    if (pack != NULL) {
        printf("协议包封装完成,长度:%u字节\n", pack_len);
        free(pack);
    }
    return 0;
}

核心亮点

  • 柔性数组:避免缓冲区冗余,适配变长数据;
  • 网络字节序:htons/htonl确保跨平台兼容性;
  • snprintf 替代 memcpy:兼顾安全性与格式化能力,避免拷贝越界。

四、总结

  1. 选型原则:除长度 100% 确定的极端场景,全量用 snprintf 替代 sprintf;
  2. 关键技巧snprintf(NULL,0,...)获取长度,动态扩容确保内容完整;
  3. 工业级要求:校验返回值、空指针,释放堆内存,工具检测兜底。
相关推荐
pas1362 小时前
34-mini-vue 更新element的children-双端对比diff算法
javascript·vue.js·算法
Qhumaing2 小时前
数据结构——例子求算法时间复杂度&&空间复杂度
数据结构·算法
萤丰信息2 小时前
智慧园区:科技赋能的未来产业生态新载体
大数据·运维·人工智能·科技·智慧园区
阿杰 AJie2 小时前
Nginx配置静态资源服务器
运维·服务器·nginx
鱼跃鹰飞2 小时前
Leetcode1027:最长等差数列
java·数据结构·算法
翱翔的苍鹰2 小时前
CIFAR-10 是一个经典的小型彩色图像分类数据集,广泛用于深度学习入门、模型验证和算法研究
深度学习·算法·分类
EverydayJoy^v^2 小时前
RH124简单知识点——第2章——调度未来任务
linux·运维
顶点多余2 小时前
静态链接 vs 动态链接,静态库 vs 动态库
linux·c++·算法
啊阿狸不会拉杆2 小时前
《机器学习》第六章-强化学习
人工智能·算法·机器学习·ai·机器人·强化学习·ml