一次 macOS 下 C++ 的 STL 踩坑記錄

背景

最近有在做 RocketMQ 社區(qū)的 Node.js SDK友瘤,是基于 RocketMQ 的 C SDK 封裝的 Addon翠肘,而 C 的 SDK 則是基于 C++ SDK 進行的封裝。

然而辫秧,卻出現(xiàn)了一個詭異的問題束倍,就是當我在消費信息的時候,發(fā)現(xiàn)在 macOS 下得到的消息居然是亂碼盟戏,也就是說 Linux 下居然是正常的绪妹。

重現(xiàn)

首先我們要知道一個函數(shù)是 const char* GetMessageTopic(CMessageExt* msg),用于從一個 msg 指針中獲取它的 Topic 信息柿究。

亂碼的代碼可以有好幾個版本邮旷,是我在排查的時候做的各種改變:

// 往 JavaScript 的 `object` 對象中插入鍵名為 `topic` 的值為 `GetMessageTopic`

// 第一種寫法:亂碼
Nan::Set(
  object, // v8 中的 JavaScript 層對象
  Nan::New("topic").ToLocalChecked(),
  Nan::New(GetMessageTopic(msg)).ToLocalChecked()
);

// 另一種寫法:亂碼
const char* temp = GetMessageTopic(msg);
Nan::Set(
  object, // v8 中的 JavaScript 層對象
  Nan::New("topic").ToLocalChecked(),
  Nan::New(temp).ToLocalChecked()
);

// 第三種寫法:亂碼
string GetMessageColumn(CMessageExt* msg, char* name)
{
  // ...

  const char* orig = GetMessageTopic(msg);
  int len = strlen(orig);
  char temp[len + 1];
  memcpy(temp, orig, sizeof(char) * (len + 1));
  return temp;
}

const char* temp = GetMessageColumn(msg, "topic");
Nan::Set(
  object, // v8 中的 JavaScript 層對象
  Nan::New("topic").ToLocalChecked(),
  Nan::New(temp).ToLocalChecked()
);

并且很詭異的是,當我在調(diào)試第三種寫法的時候蝇摸,我發(fā)現(xiàn)在 const char* orig = GetMessageTopic(msg); 這一部的時候 orig 的值是正確的婶肩。而一步步單步運行下去,一直到 memcpy 執(zhí)行結束的時候貌夕,orig 內(nèi)存塊里面的字符串居然被莫名其妙修改成亂碼了律歼。

參考如下:

image

這就不能忍了。

當我鍥而不舍的時候啡专,發(fā)現(xiàn)當我改成這樣之后险毁,返回的值就對了:

string GetMessageColumn(CMessageExt* msg, char* name)
{
  // ...

  const char* orig = GetMessageTopic(msg);
  int len = strlen(orig);
  int i;
  char temp[len + 1];
  for(i = 0; i < len + 1; i++)
  {
    temp[i] = orig[i];
  }

  // 做一些其它操作

  return temp;
}

const char* temp = GetMessageColumn(msg, "topic");
Nan::Set(
  object, // v8 中的 JavaScript 層對象
  Nan::New("topic").ToLocalChecked(),
  Nan::New(temp).ToLocalChecked()
);

但問題在于,在“其它操作”中们童,orig 還是會變成一堆亂碼畔况。當前返回能正確的原因是因為我在它變成亂碼之前,用可以“不觸發(fā)”變成亂碼的操作先把 orig 的字符串給賦值到另一個字符數(shù)組中慧库,最后返回那個新的數(shù)組跷跪。

問題看似解決了,但是這種詭異完沪、危險的行為始終是我心中的一顆喪門釘域庇,不處理總之是慌的嵌戈。

RocketMQ C++ SDK 源碼查看

在排查的過程中,我去看了 RocketMQ 的 C++ 和 C SDK 的實現(xiàn)听皿,我把重要的內(nèi)容摘出來:

class MQMessage {
public:
  string::string getTopic() const {
    return m_topic;
  }

  ...

private:
  string m_topic;

  ...
}

// MQMessageExt 是繼承自 MQMessage

const char* GetMessageTopic(CMessageExt *msg) {
    ...
    return ((MQMessageExt *) msg)->getTopic().c_str();
}

我們閱讀一下這段代碼熟呛,在 GetMessageTopic 中,先得到了一個 getTopic 的 STL 字符串尉姨,然后調(diào)用它的 c_str() 返回 const char*庵朝。一切看起來是那么美好,沒有問題又厉。

但我后來在多次調(diào)試的時候發(fā)現(xiàn)九府,對于同一個 msg 進行調(diào)用 GetMessageTopic 得到的指針居然不一樣!我是不是發(fā)現(xiàn)了什么新大陸覆致?

誠然侄旬,msg->getTopic() 返回了一個字符串對象公条,并且是通過拷貝構造從 m_topic 那邊來的帖汞。依稀記得大學時候看的 STL 源碼解析窟扑,根據(jù) STL 字符串的 Copy-On-Write 來說猎荠,我沒做任何改變的情況下,它們不應該是同源的嗎摹芙?

事實證明嵌莉,我當時的這個“想當然”就差點讓我查不出問題來了苛茂。

柳暗花明

在我捉雞了好久之后一直毫無頭緒之后之宿,在參考資料 1 中獲得了靈感族操,我開始打開腦洞(請原諒我這個坑還找了很久,畢竟我主手武器還是 Node.js)比被,會不會現(xiàn)在的 String 都不是 Copy-On-Write 了色难?但是 Linux 下又是正常的哇。

后來我在網(wǎng)上找是不是有人跟我遇到一樣的問題等缀,最后還是找到了端倪莱预。

不同的 stl 標準庫實現(xiàn)不同, 比如 CentOS 6.5 默認的 stl::string 實現(xiàn)就是 『Copy-On-Write』项滑, 而 macOS(10.10.5)實現(xiàn)就是『Eager-Copy』。

說得白話一點就是涯贞,不同庫實現(xiàn)不一樣枪狂。Linux 用的是 libstdc++,而 macOS 則是 libc++宋渔。而 libc++ 的 String 實現(xiàn)中州疾,是不寫時拷貝的,一開始賦值就采用深拷貝皇拣。也就是說就算是兩個一樣的字符串严蓖,在不同的兩個 String 對象中也不會是同源薄嫡。

其實深挖的話內(nèi)容還有很多的,例如《Effective STL》中的第 15 條也有提及 String 實現(xiàn)有多樣性颗胡;以及大多數(shù)的現(xiàn)代編譯器中 String 也都有了 Short String Optimization 的特性毫深;等等。

回到亂碼 Bug

得到了上面的結論之后毒姨,這個 Bug 的原因就知道了哑蔫。

((MQMessageExt *) msg)->getTopic() 得到了一個函數(shù)中的棧內(nèi)存字符串變量。

  • 在 Linux 中弧呐,就算是棧內(nèi)存變量闸迷,但是它的 c_str() 還是源字符串指向的指針,所以函數(shù)聲明周期結束俘枫,這個棧內(nèi)存中的字符串被釋放腥沽,c_str() 指向的內(nèi)存還堅挺著;
  • 在 macOS 下鸠蚪,由于字符串是棧內(nèi)存分配的今阳,字符串又是深拷貝,所以 c_str() 的生命周期是跟著字符串本身來的邓嘹,一旦函數(shù)調(diào)用結束酣栈,該字符串就被釋放了,相應地 c_str() 對應內(nèi)存中的內(nèi)容也被釋放汹押。

綜上所述矿筝,在 macOS 下,我通過 GetMessageTopic() 得到的內(nèi)容其實是一個已經(jīng)被釋放內(nèi)存的地址棚贾。雖然通過 for 可以趁它的內(nèi)存塊被復制之前趕緊搶救出來窖维,但是這種操作一塊已經(jīng)被釋放的內(nèi)存行為總歸是危險的,因為它的內(nèi)存塊隨時可能被覆蓋妙痹,這也就是之前亂碼的本質(zhì)了铸史。

更小 Demo 驗證

對于 STL 在這兩個平臺上不同的行為,我也抽出了一個最小化的 Demo怯伊,各位看官可以在自己的電腦上試試看:

#include <stdio.h>
#include <string>
using namespace std;

string a = "123";

string func1()
{
    return a;
}

int main()
{
    printf("0x%.8X 0x%.8X\n", a.c_str(), func1().c_str());
    return 0;
}

上面的代碼在 Linux 下(如 Ubuntu 14.04)運行會輸出兩個一樣的指針地址琳轿,而在 macOS 下執(zhí)行則輸出的是兩個不一樣的指針。

小結

在語言耿芹、庫的使用中崭篡,我們不能去使用一個沒有明確在文檔中定義的行為的“特性”。例如文檔中沒跟你說它用的是 Copy-On-Write 技術吧秕,也就說明它可能在未來任何時候不通知你就去改掉琉闪,而你也不容易去發(fā)現(xiàn)它。你就去用已經(jīng)定義好的行為即可砸彬,就是說 c_str() 返回的是字符串的一個真實內(nèi)容颠毙,我們就要認為它是跟隨著 String 的生命周期斯入,哪怕它其中有黑科技。

畢竟蛀蜜,下面這個才是 C++ reference 中提到的定義刻两,我們不能臆想人家一定是 COW 行為:

Returns a pointer to a null-terminated character array with data equivalent to those stored in the string.

The pointer is such that the range [c_str(); c_str() + size()] is valid and the values in it correspond to the values stored in the string with an additional null character after the last position.

這一樣可以引申到 JavaScript 上來,例如較早的 ECMAScript 262 第三版對于一個對象的定義中涵防,鍵名在對象中的順序也是未定義的闹伪,當時就不能討巧地看哪個瀏覽器是怎么樣一個順序來進行輸出,畢竟對于未定義的行為壮池,瀏覽器隨時改了你也不能聲討它什么偏瓤。

好久沒寫文了,碼字能力變?nèi)趿恕?/p>

以上椰憋。

參考資料

  1. Why does calling c_str() on a function that returns a string not work?
  2. Why a new C++ Standard Library for C++11?
  3. 《Effective STL》第 15 條:注意 String 實現(xiàn)的多樣性
  4. C++ 之 stl::string 寫時拷貝導致的問題
  5. C++ 再探 String 之eager-copy厅克、COW 和 SSO 方案
  6. C++ Short String Optimization stackoverflow 回答集錦以及我的思考
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市橙依,隨后出現(xiàn)的幾起案子证舟,更是在濱河造成了極大的恐慌,老刑警劉巖窗骑,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件女责,死亡現(xiàn)場離奇詭異,居然都是意外死亡创译,警方通過查閱死者的電腦和手機抵知,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來软族,“玉大人刷喜,你說我怎么就攤上這事×⒃遥” “怎么了掖疮?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颗祝。 經(jīng)常有香客問我浊闪,道長,這世上最難降的妖魔是什么螺戳? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任规揪,我火速辦了婚禮,結果婚禮上温峭,老公的妹妹穿的比我還像新娘。我一直安慰自己字支,他們只是感情好凤藏,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布奸忽。 她就那樣靜靜地躺著,像睡著了一般揖庄。 火紅的嫁衣襯著肌膚如雪栗菜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天蹄梢,我揣著相機與錄音疙筹,去河邊找鬼。 笑死禁炒,一個胖子當著我的面吹牛而咆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幕袱,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼暴备,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了们豌?” 一聲冷哼從身側響起涯捻,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎望迎,沒想到半個月后障癌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡辩尊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年涛浙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片对省。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡蝗拿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒿涎,到底是詐尸還是另有隱情哀托,我是刑警寧澤劳秋,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布仓手,位于F島的核電站,受9級特大地震影響玻淑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜添坊,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一阳准、第九天 我趴在偏房一處隱蔽的房頂上張望绕沈。 院中可真熱鬧掘鄙,春花似錦、人聲如沸吻贿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舅列。三九已至,卻和暖如春卧蜓,著一層夾襖步出監(jiān)牢的瞬間帐要,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工弥奸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留榨惠,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像冒冬,于是被迫代替她去往敵國和親伸蚯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容