C 語言字符串以「以 \0 結尾的字符數組」定義,標準庫函數 (strcpy, sprintf 等) **不檢查目標緩衝區邊界**,是緩衝區溢出、內存越界讀寫的頭號元兇。本文通過實測揭示 strcpy/strncpy/sprintf/_snprintf 的隱性缺陷,構建**邊界安全、保證 \0 終止、返回有效長度**的安全替代函數。
strcpy — 完全無保護
char dst[128], src[256];
memset(dst, '*', 128); dst[127]='\0';
memset(src, '#', 256); src[255]='\0';
strcpy(dst, src); // 緩衝區溢出,程序崩潰
strncpy — 有長度限制但**不保證 \0 終止**
strncpy(dst, src, 128); // src 長 256,只拷前 128 字節
// dst 無 \0 結尾!後續 printf/strlen 會越界讀取,輸出亂碼
實測現象:strncpy(dst, src, 8) 只拷 8 字節,dst 末尾無 \0,打印顯示 ########*******... (8個 # 後接原 dst 殘留 *)。
xg_strncpy 設計目標dst, src, dst_size (目標緩衝區總大小)min(strlen(src)+1, dst_size)dst[拷貝長度-1] = '\0'\0)
size_t xg_strncpy(char *dst, const char *src, size_t dst_size) {
if (dst_size == 0) return 0;
size_t src_len = strlen(src);
size_t copy_len = (src_len + 1 < dst_size) ? (src_len + 1) : dst_size;
memcpy(dst, src, copy_len - 1);
dst[copy_len - 1] = '\0';
return copy_len;
}
關鍵修正:原文 memcpy(dst, src, dst_size); dst[dst_size-1]='\0' 會**越界讀取** src (若 src 短於 dst_size),Debug 模式下觸發內存讀越界錯誤。必須先計算實際拷貝長度。
sprintf — 無邊界檢查,極危險同 strcpy,長字符串格式化直接溢出。
_snprintf — 有長度限制但**雙重缺陷**\0** (同 strncpy)offset += _snprintf(buf+offset, ...) 在截斷時 offset 減少,寫向 buf[-1] → **緩衝區下溢**xg_printf 設計printf 格式字符串_vsnprintf + 手動補 \0\0)**,便於安全累積
int xg_printf(char *buf, size_t buf_size, const char *fmt, ...) {
if (buf_size == 0) return 0;
va_list ap;
va_start(ap, fmt);
int written = _vsnprintf(buf, buf_size - 1, fmt, ap); // 預留 1 字節給 \0
va_end(ap);
buf[buf_size - 1] = '\0'; // 強制終止
return (written < 0) ? (int)strlen(buf) : written; // 修正返回值
}
| 不安全函數 | C11 安全版本 | 關鍵改進 |
|---|---|---|
strcpy | strcpy_s | 檢查目標大小、保證 \0、返回錯誤碼 |
strncpy | strncpy_s | 同上,補齊 \0 |
sprintf | sprintf_s | 需目標尺寸參數 |
snprintf | snprintf_s | 保證 \0、返回寫入長度 |
strcat | strcat_s | 檢查剩餘空間 |
gets | gets_s | 移除危險 gets |
現狀:MSVC 完整支持 _s 系列;GCC/Clang 需啟用 __STDC_WANT_LIB_EXT1__ 且實現不完整。跨平台項目建議**自行封裝**或使用成熟庫 (如 safeclib)。
strcpy/sprintf/strcat/getsC 語言字符串安全的核心不在於「用哪個函數」,而在於**建立「每個緩衝區都有明確擁有者、明確大小、明確邊界檢查」的工程紀律**。安全封裝函數只是紀律的代碼體現。