【轉】C/C++ 變量與內存(下篇)

引言

【上一篇】文章中介紹到 C/C++ 中一些基本類型變量在內存中的位置皮假,本來想把字符串也在上一篇中介紹的粘招,但測試發(fā)現(xiàn)字符串的情況比基本類型變量復雜的多,所以單獨寫一篇專門來介紹字符串拌喉。
這里說的字符串復雜是因為字符串實際就是 char 數(shù)組派诬,且經(jīng)常要和指針打交道盟萨,甚至不同編譯器還有不同的表現(xiàn),不是一句兩句能說清楚的兰伤。本文旨在介紹一些常見情況下字符串實際在內存中的位置(段内颗、棧和堆),讀取字符串的過程敦腔,和一些需要注意的問題均澳;本文編譯器使用 clang 和 gcc 兩種(默認使用 clang ),用以介紹不同編譯器的不同點会烙。

開發(fā)環(huán)境

  • OS X El Captian (10.11.6)
  • Apple LLVM 7.3.0 (clang-703.0.31)
  • gcc 6.1.0

字符串在內存中的位置

字符串的初始化一般有兩種方法:

  • 數(shù)組:char str[] = "Hello";
  • 指針:char *str = "World";

這里考慮全局變量负懦、靜態(tài)局部變量和局部變量三種情況;對于全局變量和靜態(tài)局部變量柏腻,還考慮已初始化和未初始化的情況纸厉。

代碼:

#include <unistd.h>

char global_unin_t1[] = {};
char *global_unin_t2;
char global_t1[] = "Hello";
char *global_t2 = "World";

int main(int argc, const char *argv[]) {
  static char local_stat_unin_t1[] = {};
  static char *local_stat_unin_t2;
  static char local_stat_t1[] = "Think";
  static char *local_stat_t2 = "Free";

  char local_t1[] = "Here";
  char *local_t2 = "Comes";

  sleep(-1);
  return 0;
}

先看看幾個典型的段信息:

sectname segname addr size type
__cstring __TEXT 0x0000000100000f9e 0x0000000000000016 S_CSTRING_LITERALS
__data __DATA 0x0000000100001018 0x0000000000000020 S_REGULAR
__common __DATA 0x0000000100001038 0x0000000000000010 S_ZEROFILL
__bss __DATA 0x0000000100001048 0x0000000000000010 S_ZEROFILL

再看看段內數(shù)據(jù):

__cstring:

addr start size data (0x) data (ASCII)
0x100000fae 6 43 6F 6D 65 73 00 Comes
0x100000fa4 5 46 72 65 65 00 Free
0x100000fa9 5 48 65 72 65 00 Here
0x100000f9e 6 57 6F 72 6C 64 00 World

gcc 編譯 __cstring 段中僅有 "World"、"Free" 和 "Comes"五嫂,沒有 "Here"颗品。

__data:

addr start size data (0x) data (ASCII)
0x100001018 6 48 65 6C 6C 6F 00 Hello
0x100001020 8 9E 0F 00 00 01 00 00 00 -
0x100001028 6 54 68 69 6E 6B 00 Think
0x100001030 8 A4 0F 00 00 01 00 00 00 -

全局變量

變量名 內存地址 所屬段 值(十六進制) 值(ASCII/0x) 類型
global_unin_t1 0x100001038 __common 00 char []
global_t1 0x100001018 __data 48 65 6C 6C 6F 00 Hello char [6]
global_t2 0x100001020 __data 9E 0F 00 00 01 00 00 00 0x100000f9e char *
global_unin_t2 0x100001040 __common 00 00 00 00 00 00 00 00 NULL char *

未初始化的全局變量放在 __common 段中肯尺,已初始化的放在 __data 段中。采用數(shù)組方式初始化的 global_t1 將本數(shù)組內的所有值都放在 __data 段中躯枢;而采用指針方式初始化的 global_t2 則僅將指針放在 __data 段中则吟,其指向 __cstring 段中的 "World"。

靜態(tài)變量

變量名 內存地址 所屬段 值(十六進制) 值(ASCII/0x) 類型
local_stat_unin_t1 0x100001048 __bss 00 char []
local_stat_unin_t2 0x100001050 __bss 00 00 00 00 00 00 00 00 NULL char *
local_stat_t1 0x100001028 __data 54 68 69 6E 6B 00 Think char [6]
local_stat_t2 0x100001030 __data A4 0F 00 00 01 00 00 00 0x100000fa4 char *

未初始化的靜態(tài)局部變量在 __bss 段中锄蹂,已初始化的放在 __data 段中氓仲。采用數(shù)組方式初始化的 local_stat_t1 將本數(shù)組內的所有值都放在 __data 段中;而采用指針方式初始化的 local_stat_t2 則僅將指針放在 __data 段中得糜,其指向 __cstring 段中的 "Free"敬扛。

局部變量

分析局部變量我們直接看反匯編代碼,將 clang 和 gcc 分開分析朝抖。

clang

char local_t1[] = "Here";

0x100000f52 <+34>: movl   0x51(%rip), %edi          ; "Here"
0x100000f58 <+40>: movl   %edi, -0x15(%rbp)
0x100000f5b <+43>: movb   0x4c(%rip), %dl           ; ""
0x100000f61 <+49>: movb   %dl, -0x11(%rbp)

這里 rip 為指令指針寄存器啥箭,rbp 為幀指針(Frame Pointer)寄存器;mov 用來傳送數(shù)據(jù)治宣,movl 操作 32 位急侥,movb 操作 8 位。

rip 為下一指令地址即 0x100000f58侮邀,所以 0x51(%rip) 表示的就是 0x100000fa9坏怪,即 __cstring 段中的 "Here"。注意到 "Here" 實際共 5 byte绊茧,不能用 movx 指令一次移動完陕悬,所以分兩次移動,一次 4 byte按傅,一次 1 byte,這樣將 "Here" 放到 -0x11(%rbp) 開始的棧中胧卤。即 local_t1 將本數(shù)組內的所有值都放棧中唯绍。

char *local_t2 = "Comes";

0x100000f3d <+13>: leaq   0x6a(%rip), %rcx          ; "Comes"
0x100000f64 <+52>: movq   %rcx, -0x20(%rbp)

這里 rip 為指令指針寄存器,rbp 為幀指針(Frame Pointer)寄存器枝誊;lea 將地址指針寫入到寄存器况芒,leaq 操作 64 位。

0x6a(%rip) 表示的是 0x100000fae叶撒,即 __cstring 段中的 "Comes"绝骚,最終這個地址被放到 -0x20(%rbp) 開始的棧中。即 local_t2 將指向 "Comes" 的指針放在棧中祠够。

gcc

char local_t1[] = "Here";

0000000100000f57           movl       $0x65726548, -0x10(%rbp) ## imm = 0x65726548
0000000100000f5e           movb       $0x0, -0xc(%rbp)

這里直接將 "Here "的二進制值 48 65 72 65 00 硬編碼到匯編中压汪,將其壓入棧中。和 clang 一樣古瓤,local_t1將本數(shù)組內的所有值都放棧中止剖。

char *local_t2 = "Comes";

0000000100000f62           leaq       0x3b(%rip), %rax        ## literal pool for: "Comes"
0000000100000f69           movq       %rax, -0x8(%rbp)

這里和 clang 一樣腺阳,不作過多解釋。local_t2 將指向 "Comes" 的指針放在棧中穿香。

先說結論:采用數(shù)組方式初始化的 local_t1 將本數(shù)組內的所有值都放在棧中亭引;而采用指針方式初始化的 local_t2 則僅將指針放在棧段中,其指向 __cstring 段中的 "Comes"皮获。再談談 clang 和 gcc 的區(qū)別焙蚓,主要就是在對以指針方式初始化的字符串上:clang 編譯時將字符串放在 __cstring 段中,運行時從 __cstring 段復制到棧中洒宝;gcc 則在編譯時直接將字符串放在代碼段中购公,運行時直接壓棧。兩者的處理方式各有優(yōu)缺點:clang 避免了將字符串常量放到代碼段中待德,若代碼中還有相同的字符串則可以合并為一個字符串君丁,但運行時有復制的步驟,存在效率問題将宪;gcc 直接將字符串放在代碼段中绘闷,沒有如上 clang 的好處,但運行時直接將值寫入棧中较坛,相對效率更高印蔗。

小結

可以看出,與基本類型變量不同的是丑勤,字符串變量用到了 __cstring 段用來保存字符串常量华嘹;但如果仔細分析,其實字符串同基本類型變量也沒有太大差別法竞,都遵循基本的規(guī)則耙厚。在對以數(shù)組方式初始化的局部變量的處理上,gcc 更常見岔霸,即將字符串以普通數(shù)組的方式編譯薛躬;但 clang 則采用了一種“融合”的方法,將字符串所特有的 __cstring 段也利用了起來呆细。

注意:這里沒有介紹字符串在堆中的情況型宝,因為在堆中的情況很簡單:malloc() 申請堆空間,返回空間指針作為字符串頭絮爷,按照常規(guī)方式讀寫或使用 string 庫操作趴酣。

不同位置讀取效率

這里僅考慮局部變量,分析兩種初始化方式帶來的讀取效率差異坑夯。因為gcc和clang處理方法基本一樣岖寞,所以這里只拿 clang 來舉例。

代碼:

#include <unistd.h>

int main(int argc, const char *argv[]) {
  char str1[] = "Hello";
  char *str2 = "World";

  char str1_0 = str1[0];
  char str1_1 = str1[1];

  char str2_0 = str2[0];
  char str2_1 = str2[1];

  sleep(-1);
  return 0;
}

數(shù)組方式

char str1_0 = str1[0];
char str1_1 = str1[1];

對應的匯編:

0x100000f4a <+58>:  movb   -0x16(%rbp), %r8b
0x100000f4e <+62>:  movb   %r8b, -0x21(%rbp)
0x100000f52 <+66>:  movb   -0x15(%rbp), %r8b
0x100000f56 <+70>:  movb   %r8b, -0x22(%rbp)

這段匯編代碼很容易(注意字符串數(shù)組就在棧里):

  1. 取棧中對應位置的數(shù)據(jù)渊涝,交給一個寄存器慎璧;
  2. 由寄存器將數(shù)據(jù)交給棧床嫌。

為什么要一個寄存器繞一下?因為棧在內存中胸私,不能直接內存到內存厌处,需要寄存器作為中間人。

指針方式

char str2_0 = str2[0];
char str2_1 = str2[1];

對應的匯編:

0x100000f5a <+74>:  movq   -0x20(%rbp), %rcx
0x100000f5e <+78>:  movb   (%rcx), %r8b
0x100000f61 <+81>:  movb   %r8b, -0x23(%rbp)
0x100000f65 <+85>:  movq   -0x20(%rbp), %rcx
0x100000f69 <+89>:  movb   0x1(%rcx), %r8b
0x100000f6d <+93>:  movb   %r8b, -0x24(%rbp)

這段匯編代碼多個一個步驟(注意字符串在__cstring段中):

  1. 從棧中取指向字符串的指針岁疼,交給一個寄存器阔涉;
  2. 取指針所指地址對應位置數(shù)據(jù),交給一個寄存器捷绒;
  3. 由寄存器將數(shù)據(jù)交給棧瑰排。

為什么這里多繞一步?因為涉及到指針所指地址轉換為地址的問題暖侨。

小結

對比兩種方式可以發(fā)現(xiàn)椭住,讀取字符串時,數(shù)組方式只需要用到一個寄存器字逗,而指針方式需要用到兩個寄存器(需要先把指針讀到寄存器)京郑,讀取以數(shù)組方式初始化的字符串要比以指針方式初始化的字符串效率高。但以指針方式初始化的字符串在加載時就已經(jīng)在內存中葫掉,而以數(shù)組方式初始化的字符串還要進行壓棧的操作些举。

寫字符串時的危險

這里介紹字符串在棧中時,可能對程序造成的破壞俭厚。實際介紹的就是 C 中危險的指針操作户魏,不僅限于字符串,只要涉及到指針操作都可能造成下面說到的破壞挪挤。

例一

在棧中時叼丑,指向字符串的指針不僅能夠修改字符串本身,還能不經(jīng)意間修改其他變量的值扛门。

#include <stdio.h>

int main(int argc, const char *argv[]) {
  int a = 1;
  char str[] = "World";
  int b = 2;

  char *str_ptr = &str[0];
  *(str_ptr + 6) = 'W';
  *(str_ptr - 3) = 'o'; // align

  printf("a=%d, b=%d, str=\"%s\"\n", a, b, str);

  return 0;
}

輸出為:

a=87, b=1862270978, str="World"

簡單解釋下幢码,因為 a 先入棧,"World" 隨后入棧尖飞,b 最后入棧,此時指針 str_ptr 指向 'W'店雅。注意棧的增長方向是像內存地址更小的方向政基,str_ptr + 6 已經(jīng)超出字符串范圍,到達了 b 的內存區(qū)域闹啦,這時在修改b的值沮明;str_ptr - 3 修改的是 a 的最低一個 byte,所以 a 的值變?yōu)?'o'窍奋,即 87荐健,至于為什么不是 str_ptr - 1酱畅,是因為涉及到內存對齊的問題。

例二

C 中函數(shù)調用時的返回地址江场、保存的寄存器的值等也是在棧中纺酸,指針操作還能威脅到整個程序的順利運行。

void write_str(char *str) {
  for (int i = 2; i < 40; i++) {
    *(str + i) = 'd';
  }
}
int main(int argc, const char *argv[]) {
  char str[] = "World";
  char *str_ptr = &str[0];

  write_str(str);

  return 0;
}

程序執(zhí)行址否,在 return 0 時會提示程序出錯 Thread 1: EXC_BAD_ACCESS (code=EXC_i386_GPLFT)餐蔬。

這里是因為操作字符串指針持續(xù)向棧底方向寫數(shù)據(jù),破壞了程序的活動記錄(Activate Record)佑附,造成程序出錯樊诺。

小結

指針操作一定要慎重!


參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載音同,如需轉載請通過簡信或評論聯(lián)系作者词爬。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市权均,隨后出現(xiàn)的幾起案子顿膨,更是在濱河造成了極大的恐慌,老刑警劉巖螺句,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虽惭,死亡現(xiàn)場離奇詭異,居然都是意外死亡蛇尚,警方通過查閱死者的電腦和手機芽唇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來取劫,“玉大人匆笤,你說我怎么就攤上這事∑仔埃” “怎么了炮捧?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惦银。 經(jīng)常有香客問我咆课,道長,這世上最難降的妖魔是什么扯俱? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任书蚪,我火速辦了婚禮,結果婚禮上迅栅,老公的妹妹穿的比我還像新娘殊校。我一直安慰自己,他們只是感情好读存,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布为流。 她就那樣靜靜地躺著呕屎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪敬察。 梳的紋絲不亂的頭發(fā)上秀睛,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音静汤,去河邊找鬼琅催。 笑死,一個胖子當著我的面吹牛虫给,可吹牛的內容都是我干的藤抡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼抹估,長吁一口氣:“原來是場噩夢啊……” “哼缠黍!你這毒婦竟也來了?” 一聲冷哼從身側響起药蜻,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤瓷式,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后语泽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贸典,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年踱卵,在試婚紗的時候發(fā)現(xiàn)自己被綠了廊驼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡惋砂,死狀恐怖妒挎,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情西饵,我是刑警寧澤酝掩,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站眷柔,受9級特大地震影響期虾,放射性物質發(fā)生泄漏。R本人自食惡果不足惜驯嘱,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一彻消、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宙拉,春花似錦、人聲如沸丙笋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锥忿,卻和暖如春牛郑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敬鬓。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工淹朋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人钉答。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓础芍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親数尿。 傳聞我的和親對象是個殘疾皇子仑性,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內容