1. 程式人生 > >sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函式辨析

sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函式辨析

看了題目中的幾個函式名是不是有點頭暈?為了防止以後總在這樣的細節裡糾纏不清,今天我們就來好好地辨析一下這幾個函式的異同。

實驗環境:

Windows下使用VS2017
Linux下使用gcc4.9.4

 

為了驗證函式的安全性我們設計瞭如下結構

const int len = 4;
#pragma pack(push)
#pragma pack(1)
struct Data
{
    char buf[len];
    char guard;
    Data()
    {
        for (int i = 0; i < len; ++i)
        {
            buf[i] 
= '*'; } guard = 0xF; } void Display() { std::cout << "sizeof(Data) = " << sizeof(Data) << std::endl; std::cout << "buf = " << buf << std::endl; std::cout << "guard = " << (unsigned int)guard << std::endl;
if (guard != 0xF) { std::cout << "memory has been broken." << std::endl; } std::cout << "---------------" << std::endl; } }; #pragma pack(pop)

當我們把資料寫到Data.buf欄位中去的時候,如果發生了記憶體越界的情況,Data.gurad欄位的記憶體會被修改。我們以此來推斷函式的安全性。

一、sprintf(Linux/Windows)

Linux下的函式原型:int sprintf(char *str, const char *format, ...);
測試程式碼:

int main()
{
    Data data;
    data.Display();
    int ret = sprintf(data.buf, "%d", 12);
    std::cout << "ret = " << ret << std::endl;
    data.Display();
    std::cin.get();
    return 0;
}

在VS2017環境中,這個函式被標記為不安全的,如果使用了,編譯器會報警告,如果非要使用,必須在編譯的時候增加巨集定義:_CRT_SECURE_NO_WARNINGS,告訴編譯器忽略安全警告。在Linux下此函式可以正常使用。而且這個函式在Windows下和Linux下行為也是一樣的。具體如下:

1.當源資料的長度【小於】len,sprintf把資料完整的寫到目標記憶體,並保證尾部以0結尾,返回寫入的位元組數。此時該函式的行為是安全的。
例如:

 sprintf(data.buf, "%d", 12); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------

 

2.當源資料的長度【等於】len,sprintf把資料完整的寫到目標記憶體,並在目標記憶體的尾部多寫入一個0,返回寫入的位元組數。此時該函式已經發生拷貝越界的情況了。所以,當用戶以為分配的記憶體剛剛好滿足拷貝需求的時候,其實已經發生了潛在的風險。

例如:

 sprintf(data.buf, "%d", 1234); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 1234
guard = 0
memory has been broken.
---------------

3.當源資料的長度【大於】len,sprintf把資料完整的寫到目標記憶體,返回寫入的位元組數,壓根不管記憶體越界的情況,甚至連個錯誤碼都不返回。

例如:

 sprintf(data.buf, "%d", 123456); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 6
sizeof(Data) = 5
buf = 123456
guard = 53
memory has been broken.
---------------

總結:以上三組實驗結果,在Windows和Linux下均可以得到驗證,可見sprintf函式的安全係數幾乎為0,不推薦大家使用。
vsprintf的行為與sprintf一樣。

 

二、sprintf_s(Windows only)

為了彌補sprintf函式的不足,高版本的MSVC環境中引入了sprintf_s函式,在呼叫的時候支援使用者傳入目標記憶體的長度,函式原型可以簡略的表示為:

 int sprintf_s(char *buf, size_t buf_size, const char *format, ...); 

1.當源資料的長度【小於】len,sprintf把資料完整的寫到目標記憶體,並保證尾部以0結尾,返回寫入的位元組數。此時該函式的行為是安全的。
例如:

 sprintf_s(data.buf, len, "%d", 12); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------

2.當源資料的長度【等於】或者【大於】len的時候,呼叫此函式將會觸發斷言。Debug模式下會彈出執行時錯誤提示框,告訴使用者"Buffer too small";Release模式下程式會直接崩潰。

例如:

 sprintf_s(data.buf, len, "%d", 1234); 

Debug模式下執行,會觸發assert,如下圖:

總結:sprintf_s函式只能在Windows下使用,雖然不會出現寫壞記憶體的情況,但是會觸發assert,導致程式中斷,使用起來也要慎重。
vsprintf_s的行為與sprintf_s一樣。

 

三、_snprintf(Windows only)

也許是覺得sprintf_s也不夠安全,MSVC環境中還引入了一個名為_snprintf的函式,其函式原型和sprintf_s類似,可以表示為:

 int _snprintf(char *buf, size_t buf_size, const char *format, ...); 

其表現行為如下:
例1,當源資料的長度【小於】len,能保證完整寫入,並以0結尾,返回實際寫入的位元組數:

 _snprintf(data.buf, len, "%d", 12); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------

例2,當源資料的長度【等於】len,能保證完整寫入,結尾不做任何處理,返回實際寫入的位元組數:

 _snprintf(data.buf, len, "%d", 1234); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 1234燙燙燙
guard = 15
---------------

例3,當源資料的長度【大於】len,最多寫入【len】個字元,結尾不錯任何處理,返回【-1】:

 _snprintf(data.buf, len, "%d", 123456); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = -1
sizeof(Data) = 5
buf = 1234燙燙燙
guard = 15
---------------

總結:_snprintf函式只能在Windows下使用,最多寫入【size】個字元,永遠不破壞記憶體,也不會觸發中斷,但不能保證目標記憶體以0結尾。通過返回值可以知道函式呼叫是否成功,返回值>=0的時候,表示呼叫成功,返回了實際寫入的字元數;返回值為-1的時候,表示目標記憶體太小,導致呼叫失敗,但是已經盡力做了填充。

_vsnprintf的行為與_snprintf一樣。

 

四、snprintf(Linux/Windows)

Linux下的函式原型為:

 int snprintf(char *str, size_t size, const char *format, ...); 

這個函式在Windows和Linux下均可以使用,並且行為一致。即:最多寫入【size-1】個字元到目標記憶體,並保證以0結尾。返回值是【應該寫入的位元組數】,而不是【實際寫入的位元組數】
例1,當源資料的長度【小於】len,能保證完整寫入,並以0結尾,返回實際寫入的位元組數:

 snprintf(data.buf, len, "%d", 12); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------

例2:當源資料的長度【等於】len,實際上只寫入了【len-1】個字元,最後一個字元用0填充,但返回值卻是【len】:

 snprintf(data.buf, len, "%d", 1234); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 123
guard = 15
---------------

例3,當源資料的長度【大於】len,最多也只寫入【len-1】個字元,最後一個字元用0填充,但返回值卻是【應該要寫入的位元組數】:

 snprintf(data.buf, len, "%d", 123456); 

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 6
sizeof(Data) = 5
buf = 123
guard = 15
---------------

總結:snprintf函式,可以在Linux/Windows雙平臺下使用,最多寫入【size-1】個字元,永遠不會破壞記憶體,也不會觸發中斷,並總能保證目標記憶體能以0結尾。唯一的問題是返回值不可靠,無法推斷呼叫是否失敗。

vsnprintf的行為與snprintf一樣。

寫到這裡,sprintf系列的相關函式都講完了,貌似沒有一個完美的函式。不過既然知道了它們的具體行為,就可以根據應用場景挑選適合的函式。

 

補充:既然已經寫到這兒了,就順便利用這個機會順便把strcpy函式簇也研究一下吧。

測試程式碼:

int main()
{
    Data data;
    data.Display();
    const char * ret = strncpy(data.buf, "12345678", len);
    std::cout << "ret = " << ret << std::endl;
    data.Display();
    std::cin.get();
    return 0;
}

一、strcpy(Linux/Windows)
函式原型為:char *strcpy(char *dest, const char *src);
最古老的字串拷貝函式,原理很簡單,從源字串依次拷貝字元到目標地址,直到遇到0為止,如遇到記憶體重疊的時候,需要特殊處理。總是返回實際寫入的字元數,不會處理記憶體越界的情況,也是毫無安全性,在此不做贅述。


二、strcpy_s(Windows only)
是Windows獨有的函式,原型可以描述為:
int strcpy_s(char *dest, size_t size, const char *src);
注意返回值不再是目標字串的首地址,而是一個int。
當源字串長度【小於】或【等於】目標記憶體的時候,此函式可以安全執行,返回值為【0】,當源字串長度【大於】目標記憶體的時候,此函式會觸發assert斷言,導致程式中斷。這個函式不會導致記憶體破壞。

三、strncpy_s(Windows only)
是Windows獨有的函式,原型可以描述為:
int strncpy_s(char *dest, size_t dest_size, const char *src, size_t count);
返回值也是一個int。
這個函式除了能指定目標記憶體的大小,還能指定拷貝的字元數量,相當於做了雙重保護。
但是注意必須滿足【count <= dest_size - 1】,這個函式才能正確呼叫,否則也會觸發assert中斷。

四、strncpy(Linux/Windows)
函式原型:char *strncpy(char *dest, const char *src, size_t size);
行為與strcpy類似,從源字串依次拷貝字元到目標地址,直到遇到0或者目標記憶體已寫滿為止,最多拷貝【size】個字元。這個函式不會破壞記憶體,也不會導致程式中斷,但是無法保證目標字串以0結尾。
例如:

strncpy(data.buf, "12345", len);

輸出:

sizeof(Data) = 5
buf = ****燙燙燙
guard = 15
---------------
ret = 1234燙燙燙
sizeof(Data) = 5
buf = 1234燙燙燙
guard = 15
---------------