????????在日常寫程序中有一些功能函數是可以重復使用的,在c語言的標準庫里面也有對應的功能函數,但是那些功能函數有會有小問題然后我就整理了一下對應功能的安全函數的使用。其中前四個函數可以編譯成一個動態庫,然后在項目工程中只需要包含對應的頭文件然后就可以調用了。
1:strcmp_iot()
????????在c語言標準庫中用于比較兩個字符串是否相等提供的函數為:strcmp()函數,還有它的衍生函數strncmp()等。
strcmp()函數的函數原型為:
#include <string.h>
int strcmp(const char *s1, const char *s2);
????????其核心原理是按照ASCII值逐字比較兩個字符串,然后根據返回值確定連個字符串是否相等。當返回值為0表示兩個字符串相等,<0表示S1小于S2,>0表示S1大于S2。
隱藏風險:
1:當傳遞的參數為NULL空指針的時候使用這個函數會直接出現段錯誤進而程序崩潰。
2:判斷字符的結束的標志位為'\0',如果傳遞過來的不是以'\0'結尾,可能會出現越界的風險。
3:strcmp
本身不寫入內存,但如果比較的是一個被溢出破壞的字符串,結果不可靠。
4:區分大小寫。
????????雖然strncmp函數比較與strcmp函數具有一定的優化和安全性,但是在實際使用中也會受到'\0'的影響,所以在這里給出一個全新的安全比較函數strcmp_iot();
函數原型:
#include <stddef.h> // for size_t
#include <string.h> // for strlen
/*** 安全比較兩個字符串是否相等* * @param string1 第一個字符串指針* @param string2 第二個字符串指針* @return 0 表示兩個字符串相同;1 表示不同或任一為 NULL*/
int strcmp_iot(const char *string1, const char *string2)
{// 檢查空指針if (string1 == NULL || string2 == NULL) {return 1; // 認為不同}size_t len1 = strlen(string1);size_t len2 = strlen(string2);// 長度不同,直接返回不同if (len1 != len2) {return 1;}// 恒定時間比較,防止時序攻擊int result = 0;for (size_t i = 0; i < len1; i++) {result |= string1[i] ^ string2[i]; // 異或:相同為0,不同為非0}// 如果 result 為 0,說明所有字符都相同return result != 0;
}
這個函數的優點:
1:加上了空指針防護
2:采用長度檢查
3:恒定時間比較,防時序攻擊
4:可以在IOT設備的設備密鑰、Token、驗證碼、身份驗證、比對數據中廣泛使用,更安全。
2:strcpy_iot()
在傳統的c語言標準庫里面提供的復制函數分為兩類函數:1.字符串復制函數,2:內存復制函數。
函數 | 是否檢查長度 | 是否自動補?\0 | 適用場景 |
---|---|---|---|
strcpy | ? 否 | ? 是 | 已知目標足夠大(不推薦) |
strncpy | ? 是 | ? 否(需手動) | 安全字符串復制 |
memcpy | ? 是 | ? 否 | 任意內存塊復制 |
memmove | ? 是 | ? 否 | 支持內存重疊的復制 |
????????在程序員的實踐中有句話叫永遠不要使用strcpy函數,其他的幾個函數想要實現安全的復制函數最優解就是組合使用,我給到一個封裝好的安全的復制函數。
函數原型:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>/*** @brief 安全復制字符串到目標緩沖區* * @param dest 目標緩沖區* @param dest_size 目標緩沖區總大小(字節)* @param src 源字符串* @return int 0 = 成功, -1 = 失敗(如空指針或緩沖區太小)*/
int strcpy_iot(char *dest, size_t dest_size, const char *src)
{// 檢查空指針if (dest == NULL || src == NULL) {return -1;}// 檢查緩沖區大小if (dest_size == 0) {return -1;}// 使用 snprintf 進行安全復制// 返回值是“如果不截斷應寫入的字符數”int ret = snprintf(dest, dest_size, "%s", src);// 如果返回值 >= dest_size,說明被截斷了if (ret < 0) {// 編碼錯誤dest[0] = '\0';return -1;}if ((size_t)ret >= dest_size) {// 被截斷,可以視為失敗,或僅警告// 這里我們清空并返回失敗(嚴格模式)dest[0] = '\0';return -1;}return 0; // 成功
}
優點:
- ? 防空指針
- ? 防緩沖區溢出
- ? 自動補?
\0
- ? 返回成功/失敗狀態
- ? 使用?
snprintf
(推薦的安全方式) - ? 支持跨平臺
3:strstr_iot()
????????strstr是C 標準庫函數中用于字符串查找函數,用于在一個字符串中查找另一個子字符串的首次出現位置。其中還有一些衍生的函數:
函數 | 用途 |
---|---|
strstr(h, n) | 查找子字符串 |
strchr(s, c) | 查找單個字符 |
strrchr(s, c) | 查找字符最后一次出現 |
strcasestr(h, n) | 忽略大小寫的?strstr (Linux) |
直接使用strstr函數的話可能出現的問題有:
1:傳遞的參數為空指針而導致的段錯誤。
2:區分大小寫,容易漏匹配
3:返回的是指針,容易誤用或越界訪問
4:無法處理重疊或二進制數據中的非字符串
????????基于上面可能會出現的問題給出兩個分裝函數用于解決上面的問題:
函數原型:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>/*** @brief 安全查找子字符串(區分大小寫)* * @param haystack 主字符串(被搜索的)* @param needle 子字符串(要找的)* @return char* 匹配位置指針,未找到返回 NULL*/
const char* strstr_iot(const char *haystack, const char *needle)
{// ? 空指針檢查if (haystack == NULL) {return NULL;}if (needle == NULL) {return NULL;}// ? 空字符串處理:strstr 標準行為是返回 haystackif (needle[0] == '\0') {return haystack;}// ? 調用標準庫return strstr(haystack, needle);
}
忽略大小寫的版本(跨平臺)
/*** @brief 安全忽略大小寫的字符串查找(跨平臺)* * @param haystack 主字符串* @param needle 子字符串* @return const char* 匹配位置,未找到返回 NULL*/
const char* strcasestr_iot(const char *haystack, const char *needle)
{if (haystack == NULL || needle == NULL) {return NULL;}if (needle[0] == '\0') {return haystack;}size_t needle_len = strlen(needle);for (const char *p = haystack; *p != '\0'; p++) {const char *h = p;const char *n = needle;while (*h && *n && (tolower(*h) == tolower(*n))) {h++;n++;}if (*n == '\0') {return p; // 找到了}}return NULL; // 未找到
}
4:atoi_iot()
在c語言中將字符串轉換為整數的標準函數,雖然操作簡單但是存在巨大的安全隱患,我在初期寫程序中就被這個擺了一道,也是沒有注意其中出現的風險。
缺點:
1:atoi函數的轉喊成功與失敗的依據是函數的返回值,但是在使用中如果轉換的數據是字符0,那就會混淆轉換結果。
int b = atoi("0"); // → 0 ?
int c = atoi("xyz"); // → 0 ? 但你怎么知道是失敗了?
2:函數使用的是int類型,當超出int范圍會出現未定義現象。
3:如果傳入的數據是空指針或者空字符串就直接觸發段錯誤從而程序崩潰。
因此根據上面出現的問題,也給出相應的替代函數strtol
(字符串轉 long)是 atoi
?的安全替換函數,我在此基礎之上再進行封裝。
函數原型:
#include <stdlib.h>
#include <errno.h>
#include <limits.h>/*** @brief 安全字符串轉整數* * @param str 輸入字符串* @param result 輸出的整數值* @return int 0 = 成功, -1 = 失敗*/
int atoi_iot(const char *str, int *result)
{// 檢查空指針if (str == NULL || result == NULL) {return -1;}// 跳過前導空白(可選)while (*str == ' ' || *str == '\t') str++;// 空字符串if (*str == '\0') {return -1;}char *endptr;errno = 0;long val = strtol(str, &endptr, 10);// 檢查是否完全沒有轉換if (endptr == str) {return -1; // 無有效數字}// 檢查是否轉換了整個字符串while (*endptr == ' ' || *endptr == '\t') endptr++;if (*endptr != '\0') {return -1; // 后面有非法字符}// 檢查溢出if (errno == ERANGE || val < INT_MIN || val > INT_MAX) {return -1;}*result = (int)val;return 0;
}
最后分享一個linux系統的主函數參數解析函數
5:getopt()
在我前期的寫程序中對于主函數傳參的處理就是簡單的調用,后面設計到傳遞的參數過多就顯得很麻煩了,然后我就發現了原來還有一個主函數傳參的高級用法getopt()。
函數原型:
int getopt(int argc, char *const argv[], const char *optstring);
參數?? ?含義
argc?? ?命令行參數個數(main 函數傳入)
argv?? ?命令行參數數組(main 函數傳入)
optstring?? ?定義合法選項的字符串
optstring 的格式
每個字符代表一個有效的命令行選項。
如果某個選項需要參數(比如 -d /dev/lirc0 中的 /dev/lirc0),就在字符后加一個冒號 :。
示例:
"d:f:h"
表示:
-d 后面必須跟一個參數(如設備路徑)
-f 后面必須跟一個參數(如配置文件路徑)
-h 是一個開關型選項,不需要參數
程序舉例:
#include <unistd.h>
#include <stdio.h>int main(int argc, char *argv[])
{int opt;while ((opt = getopt(argc, argv, "i:o:v")) != -1) {switch (opt) {case 'i':printf("輸入文件: %s\n", optarg);break;case 'o':printf("輸出文件: %s\n", optarg);break;case 'v':printf("啟用詳細模式\n");break;default:fprintf(stderr, "用法: %s -i input -o output [-v]\n", argv[0]);return 1;}}return 0;
}
主函數運行傳遞的參數為:
./app -i input.txt -o output.txt -v