首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【安全函数】memcpy_s ():C 语言内存复制的安全升级与 memcpy 深度对比

【安全函数】memcpy_s ():C 语言内存复制的安全升级与 memcpy 深度对比

作者头像
用户12001910
发布2026-01-21 20:08:22
发布2026-01-21 20:08:22
1110
举报

博主简介:byte轻骑兵,现就职于国内知名科技企业,专注于嵌入式系统研发。深耕 Android、Linux、RTOS、通信协议、AIoT、物联网及 C/C++ 等领域,乐于技术交流与分享。欢迎技术交流。 主页地址byte轻骑兵-CSDN博客 微信公众号:「嵌入式硬核研究所」 邮箱:byteqqb@163.com 声明:本文为「byte轻骑兵」原创文章,未经授权禁止任何形式转载。商业合作请联系作者授权。


在 C 语言开发中,内存操作的安全性直接关系到程序的稳定性与安全性。memcpy () 作为经典的内存复制函数,虽高效却缺乏必要的安全检查,成为缓冲区溢出等漏洞的常见源头。C11 标准引入的 memcpy_s () 函数,在保留核心功能的基础上,通过全面的安全机制重构了内存复制逻辑。


一、函数简介

memcpy_s () 是 C11 标准(ISO/IEC 9899:2011)定义的边界检查接口(Bounds Checking Interfaces)之一,其核心设计目标是解决 memcpy () 存在的安全隐患。与 memcpy () 相比,memcpy_s () 并非简单的功能增强,而是从根本上重构了内存复制的安全模型。

1.1 安全特性的革命性突破

memcpy_s () 的安全设计体现在三个关键维度:

  1. 全参数验证机制:在复制操作执行前,对所有输入参数进行合法性校验,包括指针有效性、长度合理性等
  2. 明确的错误处理:通过返回值和错误码清晰报告异常情况,替代 memcpy () 的未定义行为
  3. 边界严格控制:引入源和目标内存的最大尺寸参数,彻底杜绝缓冲区溢出

1.2 与 memcpy () 的本质差异

核心特性

memcpy()

memcpy_s()

安全检查

无任何参数验证

全面检查空指针、长度溢出、边界超限

错误处理

未定义行为(通常崩溃或数据损坏)

返回错误码,可通过 errno 获取详情

边界控制

依赖调用者保证,无机制防护

通过 destmax/srcmax 参数强制控制

标准兼容性

C89 及以上

C11 及以上(需定义__STDC_WANT_LIB_EXT1__)

适用场景

内部可控环境

处理不可信输入、安全关键系统

这种设计差异使得 memcpy_s () 特别适合以下场景:

  • 处理来自用户输入、网络传输等不可信数据源
  • 安全关键型应用(如金融系统、医疗设备、工业控制)
  • 大型软件项目中的通用组件(减少人为失误风险)
  • 需要符合安全标准(如 ISO 26262、IEC 61508)的开发

二、函数原型

memcpy_s () 的函数原型在保留 memcpy () 核心功能的基础上,通过新增参数实现了安全机制的落地,其标准定义如下:

代码语言:javascript
复制
errno_t memcpy_s(void *restrict dest, rsize_t destmax, 
                const void *restrict src, rsize_t count);

2.1 参数解析与安全设计

参数

含义

安全作用

与 memcpy () 差异

dest

目标内存块起始地址

指向待写入的内存区域

同 memcpy () 的 dest 参数

destmax

目标内存块最大字节数

限制写入长度,防止溢出

memcpy () 无此参数,缺乏边界控制

src

源内存块起始地址

指向待读取的内存区域

同 memcpy () 的 src 参数

count

要复制的字节数

指定复制长度

对应 memcpy () 的 n 参数,但增加有效性检查

2.2 返回值的安全语义

与 memcpy () 返回目标指针不同,memcpy_s () 返回 errno_t 类型的错误码,这是安全机制的关键体现:

  • 0:复制成功完成
  • 非 0:发生错误,具体值对应不同错误类型(如 EINVAL 表示无效参数,ERANGE 表示长度超限)

这种设计强制调用者处理可能的错误情况,而不是像 memcpy () 那样在出现问题时默默崩溃或产生错误结果。例如在微软的实现中,常见错误码定义为:

  • EINVAL:dest 或 src 为 NULL 指针
  • ERANGE:count > destmax(目标空间不足)或 count > RSIZE_MAX(超过最大限制)

三、函数实现(伪代码)

memcpy_s () 的实现严格遵循 "安全检查优先" 的原则,在进行实际复制前执行完整的参数验证。以下伪代码清晰展示了其执行流程:

代码语言:javascript
复制
errno_t memcpy_s(void *restrict dest, rsize_t destmax,
                const void *restrict src, rsize_t count) {
    // 阶段1:参数安全验证(memcpy()无此阶段)
    if (dest == NULL || src == NULL) {
        // 空指针检查
        set_errno(EINVAL);
        return EINVAL;
    }
    
    if (count > destmax) {
        // 目标空间不足检查
        set_errno(ERANGE);
        return ERANGE;
    }
    
    if (count > RSIZE_MAX || destmax > RSIZE_MAX) {
        // 超过实现定义的最大长度
        set_errno(EOVERFLOW);
        return EOVERFLOW;
    }
    
    // 阶段2:执行复制(与memcpy()核心逻辑相似)
    char *d = (char *)dest;
    const char *s = (const char *)src;
    
    // 处理内存重叠?不,memcpy_s()与memcpy()一样不处理重叠
    for (rsize_t i = 0; i < count; i++) {
        d[i] = s[i];
    }
    
    return 0; // 成功
}

与 memcpy () 实现的关键差异

memcpy () 的典型实现如下,对比可见两者的本质区别:

代码语言:javascript
复制
void *memcpy(void *restrict dest, const void *restrict src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;
    
    for (size_t i = 0; i < n; i++) {
        d[i] = s[i];
    }
    
    return dest;
}

memcpy_s () 的实现增加了三个关键安全层:

  1. 指针有效性验证:杜绝 NULL 指针解引用导致的崩溃
  2. 边界检查:通过 destmax 确保复制长度不超过目标内存容量
  3. 长度合理性校验:防止过大的 count 值导致的整数溢出攻击

这些检查虽然增加了约 10-15% 的性能开销(在多数场景可忽略),但彻底消除了 memcpy () 固有的安全隐患。

四、使用场景

memcpy_s () 与 memcpy () 的使用场景有交集,但在安全敏感场景中,memcpy_s () 是更优选择。通过具体场景对比,可清晰展现两者的适用边界。

1. 处理不可信网络数据(memcpy_s () 更优)

网络数据往往来自不可信来源,长度和内容均不可控,此时 memcpy_s () 的安全检查至关重要:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define BUFFER_SIZE 1024
#define __STDC_WANT_LIB_EXT1__ 1

// 安全处理网络数据包
int processNetworkData(const unsigned char *packet, size_t packetLen) {
    unsigned char buffer[BUFFER_SIZE];
    errno_t err;
    
    // 提取数据包中的 payload(前2字节为长度字段)
    if (packetLen < 2) return -1;
    size_t payloadLen = (packet[0] << 8) | packet[1];
    
    // 使用memcpy_s:自动检查payloadLen是否超过buffer容量
    err = memcpy_s(buffer, BUFFER_SIZE, packet + 2, payloadLen);
    if (err != 0) {
        printf("数据处理错误: %s\n", err == ERANGE ? "缓冲区不足" : "无效参数");
        return -1;
    }
    
    // 处理数据...
    printf("成功接收 %zu 字节数据\n", payloadLen);
    return 0;
}

// 使用memcpy的不安全版本
int unsafeProcessNetworkData(const unsigned char *packet, size_t packetLen) {
    unsigned char buffer[BUFFER_SIZE];
    
    if (packetLen < 2) return -1;
    size_t payloadLen = (packet[0] << 8) | packet[1];
    
    // 无安全检查,若payloadLen > BUFFER_SIZE则导致缓冲区溢出
    memcpy(buffer, packet + 2, payloadLen);
    
    // 处理数据...
    return 0;
}

在这个场景中,memcpy_s () 能自动检测并阻止 payloadLen 超过缓冲区大小的情况,而 memcpy () 会默默执行溢出操作,可能导致程序崩溃或被利用为攻击入口。

2. 内部数据结构复制(两者皆可,各有侧重)

对于程序内部维护的已知安全数据,两种函数均可使用,但 memcpy_s () 仍能提供额外安全保障:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[32];
    float value;
} DataItem;

// 使用memcpy_s的安全复制
int copyDataItemSafe(const DataItem *src, DataItem *dest) {
    if (src == NULL || dest == NULL) return -1;
    
    // 明确指定目标大小,防止结构体扩展导致的溢出
    errno_t err = memcpy_s(dest, sizeof(DataItem), src, sizeof(DataItem));
    return (err == 0) ? 0 : -1;
}

// 使用memcpy的高效复制
void copyDataItemFast(const DataItem *src, DataItem *dest) {
    // 假设调用者已确保指针有效且类型匹配
    memcpy(dest, src, sizeof(DataItem));
}

int main() {
    DataItem src = {100, "example", 3.14f};
    DataItem dest1, dest2;
    
    // 安全复制(适合库函数等通用场景)
    if (copyDataItemSafe(&src, &dest1) == 0) {
        printf("安全复制成功: %s\n", dest1.name);
    }
    
    // 快速复制(适合性能敏感的内部模块)
    copyDataItemFast(&src, &dest2);
    printf("快速复制成功: %s\n", dest2.name);
    
    return 0;
}

3. 动态内存操作(memcpy_s () 降低风险)

动态内存分配中,大小计算错误时有发生,memcpy_s () 能有效降低此类错误的影响:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define __STDC_WANT_LIB_EXT1__ 1

// 安全的动态内存复制
void *safeCopyDynamic(const void *src, size_t srcSize, size_t copySize) {
    // 分配目标内存
    void *dest = malloc(copySize);
    if (dest == NULL) return NULL;
    
    // 使用memcpy_s确保复制不会超过目标内存大小
    errno_t err = memcpy_s(dest, copySize, src, copySize);
    if (err != 0) {
        free(dest);
        return NULL;
    }
    
    return dest;
}

int main() {
    int src[] = {1, 2, 3, 4, 5};
    size_t srcSize = sizeof(src);
    
    // 复制前3个元素(12字节)
    int *dest = (int *)safeCopyDynamic(src, srcSize, 3 * sizeof(int));
    if (dest != NULL) {
        printf("复制结果: %d, %d, %d\n", dest[0], dest[1], dest[2]);
        free(dest);
    }
    
    return 0;
}

五、注意事项

memcpy_s () 的安全机制带来了使用复杂度的增加,需特别注意以下事项,这也是与 memcpy () 使用习惯的重要区别。

1. 编译器兼容性处理

memcpy_s () 并非所有编译器都默认支持,需进行兼容性处理:

代码语言:javascript
复制
// 跨编译器兼容的memcpy_s使用方式
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>

#ifdef __STDC_LIB_EXT1__
// C11兼容环境,直接使用标准memcpy_s
#define SAFE_MEMCPY(dest, destmax, src, count) memcpy_s(dest, destmax, src, count)
#else
// 非C11环境,使用自定义安全封装
errno_t safe_memcpy_fallback(void *dest, size_t destmax, 
                           const void *src, size_t count) {
    if (dest == NULL || src == NULL || count > destmax) {
        return -1; // 模拟错误码
    }
    memcpy(dest, src, count);
    return 0;
}
#define SAFE_MEMCPY(dest, destmax, src, count) safe_memcpy_fallback(dest, destmax, src, count)
#endif

主要编译器支持情况:

  • GCC 4.9+:需定义__STDC_WANT_LIB_EXT1__=1 并使用 - std=c11
  • Clang 3.6+:支持情况类似 GCC
  • MSVC 2013+:原生支持,但部分行为与标准略有差异
  • 嵌入式编译器:需查看具体版本的支持情况

2. 错误码的正确处理

memcpy_s () 的非零返回值必须处理,这是与 memcpy () 使用习惯的最大不同:

代码语言:javascript
复制
// 错误处理的最佳实践
void handleCopyResult(errno_t err) {
    switch (err) {
        case 0:
            printf("复制成功\n");
            break;
        case EINVAL:
            printf("错误:无效指针(NULL)\n");
            // 处理空指针情况...
            break;
        case ERANGE:
            printf("错误:复制长度超过目标容量\n");
            // 处理缓冲区不足...
            break;
        default:
            printf("错误:未知错误(%d)\n", err);
            // 通用错误处理...
    }
}

// 使用示例
void example() {
    char src[] = "hello world";
    char dest[5];
    
    errno_t err = memcpy_s(dest, sizeof(dest), src, strlen(src) + 1);
    handleCopyResult(err); // 会报告"复制长度超过目标容量"
}

3. destmax 参数的准确设置

destmax 必须准确反映目标内存的实际容量,这是安全机制的核心:

代码语言:javascript
复制
// 正确与错误的destmax设置对比
void destmaxExamples() {
    int src[5] = {1, 2, 3, 4, 5};
    int dest[3]; // 容量为3个int(12字节)
    
    // 正确:destmax设置为实际容量
    memcpy_s(dest, sizeof(dest), src, 2 * sizeof(int)); // 成功
    
    // 错误1:destmax设置过大
    memcpy_s(dest, 100, src, 5 * sizeof(int)); // 实际复制会超过dest容量,返回ERANGE
    
    // 错误2:destmax设置为元素数而非字节数
    memcpy_s(dest, 3, src, 2 * sizeof(int)); // 3字节 < 8字节,返回ERANGE
}

4. 内存重叠仍需注意

与 memcpy () 相同,memcpy_s ()不处理内存重叠问题:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

#define __STDC_WANT_LIB_EXT1__ 1

int main() {
    char str[] = "abcdefgh";
    
    // 内存重叠场景:目标在源范围内
    errno_t err = memcpy_s(str + 2, 6, str, 4); 
    // 结果不确定,可能为"ababefgh"或其他错误值
    
    printf("重叠复制结果: %s\n", str);
    
    return 0;
}

解决方案:存在重叠时应使用 memmove_s ()(C11 的安全版本)。

六、示例代码:memcpy_s () 实战应用

以下通过完整示例展示 memcpy_s () 在实际开发中的应用,对比 memcpy () 的实现,凸显安全特性。

示例 1:安全的字符串处理函数

实现一个安全的字符串截取函数,对比传统实现:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define __STDC_WANT_LIB_EXT1__ 1

// 使用memcpy_s的安全字符串截取
char *safeSubstring(const char *str, size_t start, size_t length) {
    if (str == NULL || start > strlen(str)) {
        return NULL;
    }
    
    size_t strLen = strlen(str);
    size_t actualLength = (start + length > strLen) ? (strLen - start) : length;
    
    // 分配目标字符串内存(包含终止符)
    char *result = (char *)malloc(actualLength + 1);
    if (result == NULL) return NULL;
    
    // 安全复制子字符串
    errno_t err = memcpy_s(result, actualLength + 1, str + start, actualLength);
    if (err != 0) {
        free(result);
        return NULL;
    }
    
    // 添加字符串终止符
    result[actualLength] = '\0';
    return result;
}

// 使用memcpy的传统实现
char *unsafeSubstring(const char *str, size_t start, size_t length) {
    if (str == NULL || start > strlen(str)) {
        return NULL;
    }
    
    size_t strLen = strlen(str);
    size_t actualLength = (start + length > strLen) ? (strLen - start) : length;
    
    char *result = (char *)malloc(actualLength + 1);
    if (result == NULL) return NULL;
    
    // 无安全检查,若malloc失败或参数计算错误会导致问题
    memcpy(result, str + start, actualLength);
    result[actualLength] = '\0';
    return result;
}

int main() {
    const char *text = "C语言内存安全编程";
    
    // 安全截取
    char *safeSub = safeSubstring(text, 3, 6);
    printf("安全截取: %s\n", safeSub); // 输出:内存安全编
    free(safeSub);
    
    // 传统截取
    char *unsafeSub = unsafeSubstring(text, 3, 6);
    printf("传统截取: %s\n", unsafeSub); // 输出:内存安全编
    free(unsafeSub);
    
    return 0;
}

示例 2:网络数据包解析器

实现一个安全的网络数据包解析器,处理可能的恶意输入:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define __STDC_WANT_LIB_EXT1__ 1
#define MAX_PAYLOAD 512

// 数据包结构:[类型(1字节)][长度(2字节)][负载(n字节)]
typedef struct {
    uint8_t type;
    uint16_t length;
    uint8_t payload[MAX_PAYLOAD];
} NetworkPacket;

// 安全解析数据包
int parsePacketSafe(const uint8_t *rawData, size_t dataLen, NetworkPacket *packet) {
    if (rawData == NULL || packet == NULL) return -1;
    
    // 检查基础头部长度
    if (dataLen < 3) return -1;
    
    // 解析类型字段
    errno_t err = memcpy_s(&packet->type, sizeof(packet->type), rawData, 1);
    if (err != 0) return -1;
    
    // 解析长度字段
    uint16_t payloadLen;
    err = memcpy_s(&payloadLen, sizeof(payloadLen), rawData + 1, 2);
    if (err != 0) return -1;
    
    // 验证负载长度
    if (payloadLen > MAX_PAYLOAD) return -1;
    if (3 + payloadLen > dataLen) return -1; // 数据不完整
    
    // 解析负载
    packet->length = payloadLen;
    err = memcpy_s(packet->payload, MAX_PAYLOAD, rawData + 3, payloadLen);
    if (err != 0) return -1;
    
    return 0;
}

int main() {
    // 模拟一个合法数据包
    uint8_t validData[] = {0x02, 0x00, 0x05, 0x11, 0x22, 0x33, 0x44, 0x55};
    NetworkPacket packet;
    
    if (parsePacketSafe(validData, sizeof(validData), &packet) == 0) {
        printf("解析成功 - 类型: %hhu, 长度: %hu\n", packet.type, packet.length);
    }
    
    // 模拟一个过长的数据包(恶意输入)
    uint8_t maliciousData[] = {0x03, 0x02, 0x00}; // 长度字段为512
    if (parsePacketSafe(maliciousData, sizeof(maliciousData), &packet) != 0) {
        printf("成功拦截恶意数据包\n");
    }
    
    return 0;
}

memcpy_s () 与长度检查结合,有效防止了恶意构造的超长负载导致的缓冲区溢出。


memcpy_s () 作为 memcpy () 的安全升级版本,代表了 C 语言在安全性方面的重要进步。 1. 安全价值:memcpy_s () 通过参数验证、边界控制和错误处理三大机制,彻底解决了 memcpy () 的安全隐患,特别适合处理不可信数据。 2. 使用原则

  • 新代码优先使用 memcpy_s (),尤其是处理外部输入时
  • 性能敏感且内部可控的场景可保留 memcpy ()
  • 始终处理 memcpy_s () 的返回值,不忽略错误检查

3. 局限性:memcpy_s () 不处理内存重叠,需与 memmove_s () 配合使用;同时存在编译器兼容性问题,需做好兼容处理。 在软件安全日益重要的今天,从 memcpy () 到 memcpy_s () 的转变,不仅是函数的替换,更是编程理念的升级 —— 将安全从 "事后调试" 转变为 "事前预防"。掌握 memcpy_s () 的使用,是每个 C 语言开发者提升代码质量、构建安全系统的必备技能。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、函数简介
  • 二、函数原型
  • 三、函数实现(伪代码)
  • 四、使用场景
  • 五、注意事项
  • 六、示例代码:memcpy_s () 实战应用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档