C语言字符串安全查找的三种方式详解
作者:byte轻骑兵
引言
在 C 语言开发中,字符串操作是安全漏洞的 “重灾区”—— 传统函数(如strchr、strrchr、strstr)缺乏边界检查,若输入字符串未正确以\0结尾,极易触发缓冲区溢出,导致程序崩溃或被恶意利用(如注入攻击)。
为解决这一问题,C11 标准(ISO/IEC 9899:2011) 引入 “边界检查接口”(Bounds-checking interfaces),其中strchr_s、strrchr_s、strstr_s便是传统查找函数的安全增强版本。它们通过增加长度参数、明确错误处理,从根源上降低安全风险,成为安全关键型应用(如嵌入式系统、金融软件)的首选工具。
一、安全字符串函数概述
strchr_s、strrchr_s、strstr_s保留了传统函数的核心查找功能,同时新增以下安全特性:
- 强制传入字符串长度参数,限制操作范围,防止越界访问;
 - 通过返回值(错误码)报告异常,而非依赖 “未定义行为”;
 - 主动校验无效参数(如空指针、超范围长度);
 - 检查字符串是否在指定长度内正确终止(避免处理不完整字符串)。
 
注:使用这些函数需先定义宏__STDC_WANT_LIB_EXT1__(通常在包含<string.h>前),以启用 C11 标准的安全接口。
二、strchr_s:安全的正向字符查找
1. 函数简介
strchr_s用于在字符串中从左到右查找第一个匹配字符,核心优势是通过 “长度参数 + 错误校验”,确保查找不超出缓冲区边界。
2. 函数原型
errno_t strchr_s(char const* str, rsize_t strsz, int c, char const** result);
| 参数名 | 说明 | 
|---|---|
| str | 待查找的源字符串指针(需以\0结尾,且非空) | 
| strsz | 源字符串的最大长度(含\0,通常用sizeof(str)获取数组长度) | 
| c | 目标字符(虽为int类型,实际会被转换为char,兼容EOF) | 
| result | 输出参数:存储查找结果(成功则指向匹配字符,失败则为NULL) | 
返回值:
- 成功(找到 / 未找到字符):返回
0; - 失败(参数无效 / 长度异常):返回非零错误码(如
EINVAL、ESPACE)。 
错误码含义:
- :
str或result为NULL(无效指针); ESPACE:strsz为0、超过RSIZE_MAX(通常为SIZE_MAX/2),或字符串未在strsz内终止。
3. 实现逻辑(伪代码)
function strchr_s(str: const char*, strsz: rsize_t, c: int, result: const char**) -> errno_t:
    // 1. 校验无效指针
    if str == NULL or result == NULL:
        return EINVAL
    
    // 2. 校验长度合法性
    if strsz == 0 or strsz > RSIZE_MAX:
        *result = NULL
        return ESPACE
    
    // 3. 转换目标字符(int→char)
    c_char = (char)c
    
    // 4. 正向遍历查找
    for i from 0 to strsz - 1:
        if str[i] == c_char:
            *result = &str[i]  // 找到匹配,记录位置
            return 0
        if str[i] == '\0':
            break  // 已达字符串实际结尾,停止遍历
    
    // 5. 未找到字符(非错误,返回0)
    *result = NULL
    return 04. 工作流程示意图

5. 使用场景
strchr_s适用于所有需查找单个字符的场景,尤其适合:
- 处理不可信输入(如用户输入的邮箱、配置项);
 - 验证字符串格式(如检查邮箱是否含
@、路径是否含分隔符); - 分割字符串(如按
:分割键值对"name:zhangsan")。 
6. 示例代码
#define __STDC_WANT_LIB_EXT1__ 1  // 启用C11安全函数
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
    // 场景1:验证邮箱格式(查找@)
    char email[] = "user@example.com";
    const char* result;
    errno_t err;  // 存储错误码
    // 调用strchr_s查找@
    err = strchr_s(email, sizeof(email), '@', &result);
    // 错误处理
    if (err != 0) {
        printf("查找失败!错误码:%d\n", err);
        return 1;
    }
    // 结果处理
    if (result != NULL) {
        printf("找到'@',位置(索引):%td\n", result - email);
        printf("邮箱用户名:%.*s\n", (int)(result - email), email);  // 截取用户名
        printf("邮箱域名:%s\n", result + 1);                      // 截取域名
    } else {
        printf("未找到'@',邮箱格式非法!\n");
    }
    // 场景2:查找超出字符串长度的字符(边界测试)
    char buffer[10] = "test";  // 缓冲区大小10,实际字符串长度4(含\0)
    err = strchr_s(buffer, 10, 'x', &result);
    if (err == 0 && result == NULL) {
        printf("\n在buffer中未找到'x'(边界检查生效)\n");
    }
    return 0;
}运行结果:
找到'@',位置(索引):4
邮箱用户名:user
邮箱域名:example.com
在buffer中未找到'x'(边界检查生效)
7 注意事项
- 长度参数必须含
\0:strsz需包含字符串的终止符\0,如char str[] = "abc"的strsz应为4(而非3); - 错误码优先校验:不能仅通过
result是否为NULL判断成功 —— 需先检查err是否为0(如err=EINVAL时result也为NULL); c的类型转换:c为int是为兼容EOF(值为-1),函数内部会转为char,无需手动处理;- 查找
\0的特殊情况:若c='\0',函数会返回指向字符串末尾\0的指针(符合 C11 标准)。 
三、strrchr_s:安全的反向字符查找
1. 函数简介
strrchr_s是strchr_s的反向版本,用于从字符串末尾(右→左)查找最后一个匹配字符。同样通过边界检查,避免越界访问,核心场景是 “提取最右侧字符相关内容”(如文件名、后缀)。
2. 函数原型
errno_t strrchr_s(char const* str, rsize_t strsz, int c, char const** result);
参数、返回值、错误码与strchr_s完全一致,仅查找方向不同。
3. 实现逻辑(伪代码)
function strrchr_s(str: const char*, strsz: rsize_t, c: int, result: const char**) -> errno_t:
    // 1. 校验无效指针
    if str == NULL or result == NULL:
        return EINVAL
    
    // 2. 校验长度合法性
    if strsz == 0 or strsz > RSIZE_MAX:
        *result = NULL
        return ESPACE
    
    // 3. 转换目标字符
    c_char = (char)c
    
    // 4. 第一步:定位字符串实际结尾(找到\0)
    str_end = NULL
    for i from 0 to strsz - 1:
        if str[i] == '\0':
            str_end = &str[i]
            break
    if str_end == NULL:  // 字符串未在strsz内终止
        *result = NULL
        return ESPACE
    
    // 5. 第二步:从结尾反向查找
    current = str_end - 1  // 从\0的前一个字符开始
    while current >= str:
        if *current == c_char:
            *result = current
            return 0
        current = current - 1
    
    // 6. 特殊情况:查找\0
    if c_char == '\0':
        *result = str_end
        return 0
    
    // 7. 未找到字符
    *result = NULL
    return 04. 工作流程示意图

5. 使用场景
strrchr_s的核心场景是 “提取最右侧字符后的内容”,典型案例:
- 从文件路径中提取文件名(如
"/home/user/test.c"→"test.c",查找最后一个/); - 获取文件后缀(如
"image.png"→".png",查找最后一个.); - 分割带重复分隔符的字符串(如
"a,b,c,d"→"d",查找最后一个,)。 
6. 示例代码(提取文件名与后缀)
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <string.h>
#include <errno.h>
// 安全提取文件名(支持/和\路径分隔符)
const char* get_filename_s(const char* path, rsize_t path_len) {
    const char* result;
    errno_t err;
    // 第一步:查找最后一个/
    err = strrchr_s(path, path_len, '/', &result);
    if (err != 0) return NULL;  // 参数错误,返回空
    if (result != NULL) {
        return result + 1;  // 跳过/,返回文件名起始位置
    }
    // 第二步:若未找到/,查找最后一个\(Windows路径)
    err = strrchr_s(path, path_len, '\\', &result);
    if (err != 0) return NULL;
    return (result != NULL) ? (result + 1) : path;  // 无分隔符则返回原路径
}
int main() {
    // 测试不同路径格式
    char path1[] = "/home/user/documents/report.pdf";  // Linux路径
    char path2[] = "C:\\Users\\user\\photos\\vacation.jpg";  // Windows路径
    char path3[] = "readme.txt";  // 无路径(仅文件名)
    // 提取文件名
    const char* filename1 = get_filename_s(path1, sizeof(path1));
    const char* filename2 = get_filename_s(path2, sizeof(path2));
    const char* filename3 = get_filename_s(path3, sizeof(path3));
    printf("路径1的文件名:%s\n", filename1);
    printf("路径2的文件名:%s\n", filename2);
    printf("路径3的文件名:%s\n", filename3);
    // 提取文件后缀(查找最后一个.)
    const char* ext;
    errno_t err = strrchr_s(filename1, strlen(filename1) + 1, '.', &ext);
    if (err == 0 && ext != NULL) {
        printf("\n文件1的后缀:%s\n", ext);
    }
    return 0;
}运行结果:
路径1的文件名:report.pdf 路径2的文件名:vacation.jpg 路径3的文件名:readme.txt 文件1的后缀:.pdf
7 注意事项
- 路径以分隔符结尾的处理:若路径为
"/home/user/docs/"(末尾为/),result会指向该/,此时result+1为\0(空字符串),需额外判断; - 性能对比:
strrchr_s需先遍历到字符串结尾,再反向查找,效率略低于strchr_s,但安全性无差异; - 与
strchr_s的共性问题:同样需注意长度参数含\0、错误码优先校验、c的类型转换。 
四、strstr_s:安全的子串查找
1. 函数简介
strstr_s用于在主串(haystack)中查找子串(needle)的首次出现位置,是strstr的安全版本。它通过同时校验主串和子串的长度,防止 “子串越界” 或 “主串未终止” 导致的安全问题,核心场景是 “多字符匹配”(如关键词搜索、协议解析)。
2. 函数原型
errno_t strstr_s(char const* haystack, rsize_t haystacksz, 
                 char const* needle, rsize_t needlesz, 
                 char const** result);| 参数名 | 说明 | 
|---|---|
haystack | 主串(待查找的字符串,需以\0结尾) | 
haystacksz | 主串的最大长度(含\0) | 
needle | 子串(目标查找内容,需以\0结尾) | 
needlesz | 子串的最大长度(含\0) | 
result | 输出参数:成功则指向主串中子串的起始位置,失败则为NULL | 
返回值与错误码:
- 成功(找到 / 未找到子串):返回
0; - 失败:返回非零错误码(
EINVAL/ESPACE/EILSEQ)。 
新增错误码EILSEQ:子串长度(实际长度)大于主串长度,不可能匹配。
3. 实现逻辑(伪代码:朴素算法)
strstr_s的核心是 “主串遍历 + 子串逐字符比对”,以下为易理解的朴素实现(标准库通常用 KMP/BM 算法优化,时间复杂度从O(n*m)降至O(n+m)):
function strstr_s(haystack: const char*, haystacksz: rsize_t,
                 needle: const char*, needlesz: rsize_t,
                 result: const char**) -> errno_t:
    // 1. 校验无效指针
    if haystack == NULL or needle == NULL or result == NULL:
        return EINVAL
    
    // 2. 校验长度合法性
    if haystacksz == 0 or haystacksz > RSIZE_MAX or
       needlesz == 0 or needlesz > RSIZE_MAX:
        *result = NULL
        return ESPACE
    
    // 3. 计算主串/子串的实际长度(不含\0)
    haystack_len = 0
    while haystack_len < haystacksz and haystack[haystack_len] != '\0':
        haystack_len += 1
    if haystack_len == haystacksz and haystack[haystack_len-1] != '\0':
        *result = NULL
        return ESPACE  // 主串未终止
    
    needle_len = 0
    while needle_len < needlesz and needle[needle_len] != '\0':
        needle_len += 1
    if needle_len == needlesz and needle[needle_len-1] != '\0':
        *result = NULL
        return ESPACE  // 子串未终止
    
    // 4. 特殊情况:子串为空(返回主串起始位置)
    if needle_len == 0:
        *result = haystack
        return 0
    
    // 5. 子串长于主串(直接返回未找到)
    if needle_len > haystack_len:
        *result = NULL
        return 0
    
    // 6. 朴素查找:主串遍历+子串比对
    max_pos = haystack_len - needle_len  // 主串遍历的最大位置
    for i from 0 to max_pos:
        match = true
        // 逐字符比对子串
        for j from 0 to needle_len - 1:
            if haystack[i+j] != needle[j]:
                match = false
                break
        if match:
            *result = &haystack[i]
            return 0
    
    // 7. 未找到子串
    *result = NULL
    return 04. 工作流程示意图

5. 使用场景
strstr_s是多字符匹配的核心工具,典型场景:
- 文本关键词搜索(如日志中查找 “error”“warning”);
 - 协议解析(如从
"HTTP/1.1 200 OK"中提取 “200” 状态码); - URL 处理(如从
"https://github.com"中提取"github.com"); - 敏感词过滤(如用户输入中检测违规词汇)。
 
6. 示例代码(敏感词检测与 URL 解析)
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <string.h>
#include <errno.h>
// 安全检测文本是否含敏感词
int check_sensitive_word(const char* text, rsize_t text_len,
                        const char* word, rsize_t word_len) {
    const char* result;
    errno_t err = strstr_s(text, text_len, word, word_len, &result);
    if (err != 0) {
        printf("检测失败!错误码:%d\n", err);
        return -1;  // 错误标识
    }
    return (result != NULL) ? 1 : 0;  // 1=含敏感词,0=不含
}
// 安全提取URL域名(支持http/https)
const char* get_url_domain(const char* url, rsize_t url_len) {
    const char* result;
    errno_t err;
    // 先查找"https://"(长度8)
    err = strstr_s(url, url_len, "https://", 9, &result);  // 9=8+1(含\0)
    if (err == 0 && result != NULL) {
        return result + 8;
    }
    // 再查找"http://"(长度7)
    err = strstr_s(url, url_len, "http://", 8, &result);   // 8=7+1
    if (err == 0 && result != NULL) {
        return result + 7;
    }
    return url;  // 非HTTP/HTTPS协议,返回原URL
}
int main() {
    // 场景1:敏感词检测
    char log[] = "2024-06-01: user input contains 'badword'";
    char sensitive[] = "badword";
    int has_sensitive = check_sensitive_word(log, sizeof(log), 
                                            sensitive, sizeof(sensitive));
    
    if (has_sensitive == 1) {
        printf("日志含敏感词:%s\n", sensitive);
    } else if (has_sensitive == 0) {
        printf("日志无敏感词\n");
    }
    // 场景2:URL域名提取
    char url1[] = "https://github.com";
    char url2[] = "http://www.baidu.com";
    char url3[] = "ftp://ftp.example.com";  // 非HTTP协议
    const char* domain1 = get_url_domain(url1, sizeof(url1));
    const char* domain2 = get_url_domain(url2, sizeof(url2));
    const char* domain3 = get_url_domain(url3, sizeof(url3));
    printf("\nURL1域名:%s\n", domain1);
    printf("URL2域名:%s\n", domain2);
    printf("URL3域名:%s\n", domain3);
    return 0;
}运行结果:
日志含敏感词:badword URL1域名:github.com URL2域名:www.baidu.com URL3域名:ftp://ftp.example.com
7. 注意事项
- 双长度参数需同步校验:
haystacksz和needlesz均需包含各自的\0,如子串"abc"的needlesz应为4; - 子串为空的特殊处理:根据 C11 标准,若
needle为空(""),result会返回主串起始位置(而非NULL); - 大小写敏感:
strstr_s区分大小写(如"ABC"≠"abc"),如需不敏感匹配,需先将主串 / 子串统一转为大写 / 小写; - 性能优化:朴素算法在 “主串 / 子串均为 AAAAA...B” 时效率低,实际开发中可优先使用标准库实现(如 GCC 的
strstr_s用 KMP 优化)。 
五、安全函数与传统函数的差异对比
为清晰区分strchr_s/strrchr_s/strstr_s与传统函数,下表从核心特性维度对比:
| 对比维度 | strchr(传统) | strchr_s(安全) | strstr(传统) | strstr_s(安全) | 
|---|---|---|---|---|
| 函数原型 | char* strchr(const char*, int) | errno_t strchr_s(..., rsize_t, ..., **) | char* strstr(const char*, const char*) | errno_t strstr_s(..., rsize_t, ..., rsize_t, **) | 
| 返回值含义 | 直接返回查找结果(指针 / NULL) | 返回错误码(0 = 成功),结果通过输出参数获取 | 直接返回查找结果(指针 / NULL) | 返回错误码(0 = 成功),结果通过输出参数获取 | 
| 长度参数 | 无(依赖\0终止) | 有(strsz,强制边界检查) | 无(依赖\0终止) | 有(haystacksz+needlesz) | 
| 错误处理 | 未定义(如空指针会崩溃) | 明确错误码(EINVAL/ESPACE) | 未定义(如子串未终止会越界) | 明确错误码(EINVAL/ESPACE/EILSEQ) | 
| 空指针处理 | 程序崩溃或不可预测行为 | 返回 EINVAL,result=NULL | 程序崩溃或不可预测行为 | 返回 EINVAL,result=NULL | 
| 未终止字符串处理 | 越界访问(缓冲区溢出风险) | 返回 ESPACE,拒绝处理 | 越界访问(缓冲区溢出风险) | 返回 ESPACE,拒绝处理 | 
| 适用场景 | 信任输入的简单场景 | 不可信输入、安全关键场景 | 信任输入的简单场景 | 不可信输入、安全关键场景 | 
| 标准依赖 | C89 及以后(所有编译器支持) | C11 及以后(需定义__STDC_WANT_LIB_EXT1__) | C89 及以后(所有编译器支持) | C11 及以后(需定义__STDC_WANT_LIB_EXT1__) | 
六、经典面试题
问:strchr_s在什么情况下会返回 EINVAL错误?请列举至少三种情况。
答:
- 当 
str参数为NULL但strsz不为 0 时 - 当 
result输出参数为NULL指针时 - 当 
strsz参数为 0 但str不为NULL时(部分实现) - 当任何参数不满足前置条件,如指针对齐问题等
 
 问:在处理网络数据包时,为什么 strstr_s比 strstr更安全?请从攻击者角度分析。
答: 攻击者可以构造特殊数据包:
- 发送不含 
\0终止符的长字符串,导致strstr越界读取 - 精心设计偏移使查找操作跨越缓冲区边界
 - 利用越界读取获取敏感信息或导致程序崩溃
 
strstr_s通过 destsz和 srcsz双重限制,确保搜索在预定边界内进行,即使面对恶意数据也能安全处理。
问:如果项目需要同时支持 Windows 和 Linux,如何实现安全字符串函数的跨平台兼容?
答: 推荐三种方案:
- 特性检测宏:使用 
#ifdef __STDC_LIB_EXT1__检测编译器支持 - 适配层封装:实现统一接口,底层调用平台相关实现
 - 第三方库:引入 
safeclib等跨平台兼容库 
最佳实践是创建项目内的安全字符串包装层,集中处理平台差异,为业务代码提供统一接口。
问:将传统函数strchr的调用代码迁移为strchr_s,需注意哪些关键步骤?请举例说明。
答:迁移需围绕 “参数适配”“错误处理”“结果获取” 三个核心步骤,具体如下:
步骤 1:启用 C11 安全函数
在包含<string.h>前定义宏__STDC_WANT_LIB_EXT1__,确保编译器启用安全接口:
#define __STDC_WANT_LIB_EXT1__ 1 #include <string.h>
步骤 2:调整函数参数(新增长度参数与输出参数)
- 传统
strchr调用:char* pos = strchr(str, 'a'); - 安全
strchr_s需新增:① 长度参数(strsz);② 输出参数(result指针)。 
步骤 3:新增错误处理逻辑
传统函数不返回错误码,迁移后需先校验err是否为0,再处理result(避免将 “参数错误” 与 “未找到” 混淆)。
步骤 4:调整结果获取方式
strchr_s的结果通过输出参数result获取,而非直接返回。
迁移示例(从strchr到strchr_s):
传统代码:
#include <stdio.h>
#include <string.h>
int main() {
    char str[] = "name:zhangsan";
    // 传统调用:直接返回结果
    char* pos = strchr(str, ':');
    
    if (pos != NULL) {
        printf("找到':',后续内容:%s\n", pos + 1);
    } else {
        printf("未找到':'\n");
    }
    return 0;
}迁移后代码:
#define __STDC_WANT_LIB_EXT1__ 1  // 步骤1:启用安全函数
#include <stdio.h>
#include <string.h>
#include <errno.h>  // 步骤1:包含错误码头文件
int main() {
    char str[] = "name:zhangsan";
    const char* result;  // 步骤2:新增输出参数
    errno_t err;         // 步骤2:新增错误码变量
    // 步骤2:调用strchr_s,新增长度参数sizeof(str)
    err = strchr_s(str, sizeof(str), ':', &result);
    // 步骤3:优先处理错误
    if (err != 0) {
        printf("查找错误!错误码:%d\n", err);
        return 1;
    }
    // 步骤4:通过result获取结果
    if (result != NULL) {
        printf("找到':',后续内容:%s\n", result + 1);
    } else {
        printf("未找到':'\n");
    }
    return 0;
}关键注意点:
- 长度参数必须包含
\0(如sizeof(str)而非strlen(str)); - 错误码
err的校验优先级高于result(如err=EINVAL时result也为NULL,但属于参数错误,需单独处理)。 
strchr_s、strrchr_s、strstr_s是 C11 标准为解决传统字符串函数安全问题而设计的核心工具,它们通过 “强制长度参数 + 明确错误处理”,从根源上规避了缓冲区溢出等风险。
开发实践中,需根据场景选择函数:
- 简单信任输入场景:传统函数(
strchr等)更简洁; - 不可信输入(如用户输入、网络数据)或安全关键模块:必须使用安全函数(
strchr_s等)。 
掌握安全函数的使用与迁移方法,不仅能提升代码健壮性,更是 C 语言开发者应对安全面试的核心考点。
以上就是C语言字符串安全查找的三种方式详解的详细内容,更多关于C语言字符串安全查找的资料请关注脚本之家其它相关文章!
