偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

10 個內(nèi)存引發(fā)的大坑,你能躲開幾個?

存儲 存儲軟件
對程序員來說內(nèi)存相關(guān)的 bug 排查難度幾乎和多線程問題并駕齊驅(qū),當(dāng)程序出現(xiàn)運(yùn)行異常時可能距離真正有 bug 的那行代碼已經(jīng)很遠(yuǎn)了,這就導(dǎo)致問題定位排查非常困難,這篇文章將總結(jié)涉及內(nèi)存的一些經(jīng)典 bug ,快來看看你知道幾個,或者你的程序中現(xiàn)在有幾個。。。

[[382797]]

對程序員來說內(nèi)存相關(guān)的 bug 排查難度幾乎和多線程問題并駕齊驅(qū),當(dāng)程序出現(xiàn)運(yùn)行異常時可能距離真正有 bug 的那行代碼已經(jīng)很遠(yuǎn)了,這就導(dǎo)致問題定位排查非常困難,這篇文章將總結(jié)涉及內(nèi)存的一些經(jīng)典 bug ,快來看看你知道幾個,或者你的程序中現(xiàn)在有幾個。。。

返回局部變量地址

我們來看這樣一段代碼:

  1. int fun() {  int a = 2;  return &a;}void main() {  int* p = fun();  *p = 20;} 

這段代碼非常簡單,func 函數(shù)返回一個指向局部變量的地址,main 函數(shù)中調(diào)用 fun 函數(shù),獲取到指針后將其設(shè)置為 20。

你能看出這段代碼有什么問題嗎?問題在于局部變量 a 位于 func 的棧幀中,當(dāng) func 執(zhí)行結(jié)束,其棧幀也不復(fù)存在,因此 main 函數(shù)中調(diào)用 func 函數(shù)后得到的指針指向一個不存在的變量:

盡管上述代碼仍然可以“正常”運(yùn)行,但如果后續(xù)調(diào)用其它函數(shù)比如funcB,那么指針p指向的內(nèi)容將被 funcB 函數(shù)的棧幀內(nèi)容覆蓋掉,又或者修改指針 p 實際上是在破壞 funcB 函數(shù)的棧幀,這將導(dǎo)致極其難以排查的 bug。

 

錯誤的理解指針運(yùn)算

  1. int sum(int* arr, int len) { 
  2.   int sum = 0; 
  3.   for (int i = 0; i < len; i++) { 
  4.       sum += *arr; 
  5.       arr += sizeof(int); 
  6.   } 
  7.   return sum

這段代碼本意是想計算給定數(shù)組的和,但上述代碼并沒有理解指針運(yùn)算的本意。指針運(yùn)算中的加1并不是說移動一個字節(jié)而是移動一個單位,指針指向的數(shù)據(jù)結(jié)構(gòu)大小就是一個單位。因此,如果指針指向的數(shù)據(jù)類型是 int,那么指針加 1 則移動 4 個字節(jié)(32位),如果指針指向的是結(jié)構(gòu)體,該結(jié)構(gòu)體的大小為 1024 字節(jié),那么指針加 1 其實是移動 1024 字節(jié)。


 

從這里我們可以看出,移動指針時我們根本不需要關(guān)心指針指向的數(shù)據(jù)類型的大小,因此上述代碼簡單的將arr += sizeof(int)改為arr++即可。

 

解引用有問題的指針

C語言初學(xué)者常會犯一個經(jīng)典錯誤,那就是從標(biāo)準(zhǔn)輸入中獲取鍵盤數(shù)據(jù),代碼是這樣寫的:

  1. int a; 
  2. scanf("%d", a); 

很多同學(xué)并不知道這樣寫會有什么問題,因為上述代碼有時并不會出現(xiàn)運(yùn)行時錯誤。

原來 scanf 會將a的值當(dāng)做地址來對待,并將從標(biāo)準(zhǔn)輸入中獲取到的數(shù)據(jù)寫到該地址中。這時接下來程序的表現(xiàn)就取決于a的值了,而上述代碼中局部變量a的值是不確定的,那么這時:

如果a的值作為指針指向代碼區(qū)或者其它不可寫區(qū)域,操作系統(tǒng)將立刻kill掉該進(jìn)程,這是最好的情況,這時發(fā)現(xiàn)問題還不算很難

如果a的值作為指針指向棧區(qū),那么此時恭喜你,其它函數(shù)的棧幀已經(jīng)被破壞掉了,那么程序接下來的行為將脫離掌控,這樣的 bug 極難定位

如果a的值作為指針指向堆區(qū),那么此時也恭喜你,代碼中動態(tài)分配的內(nèi)存已經(jīng)被你破壞掉了,那么程序接下來的行為同樣脫離掌控,這樣的bug也極難定位

 

讀取未初始化的內(nèi)存

我們來看這樣一段代碼:

  1. void add() { 
  2.   int* a = (int*)malloc(sizeof(int)); 
  3.   *a += 10; 

上述代碼的錯誤之處在于假設(shè)從堆上動態(tài)分配的內(nèi)存總是初始化為 0,實際上并不是這樣的。我們需要知道,當(dāng)調(diào)用 malloc 時實際上有以下兩種可能:

如果 malloc 自己維護(hù)的內(nèi)存夠用,那么 malloc 從空閑內(nèi)存中找到一塊大小合適的返回,注意,這一塊內(nèi)存可能是之前用過后釋放的。在這種情況下,這塊內(nèi)存包含了上次使用時留下的信息,因此不一定為0

如果 malloc 自己維護(hù)的內(nèi)存不夠用,那么通過 brk 等系統(tǒng)調(diào)用向操作系統(tǒng)申請內(nèi)存,在這種情況下操作系統(tǒng)返回的內(nèi)存確實會被初始化為0。原因很簡單,操作系統(tǒng)返回的這塊內(nèi)存可能之前被其它進(jìn)程使用過,這里面也許會包含了一些敏感信息,像密碼之類,因此出于安全考慮防止你讀取到其它進(jìn)程的信息,操作系統(tǒng)在把內(nèi)存交給你之前會將其初始化為0。

現(xiàn)在你應(yīng)該知道了吧,你不能想當(dāng)然的假定 malloc 返回給你的內(nèi)存已經(jīng)被初始化為 0,你需要自己手動清空。

 

內(nèi)存泄

  1. void memory_leak() { 
  2.   int *p = (int *)malloc(sizeof(int)); 
  3.   return

上述代碼在申請一段內(nèi)存后直接返回,這樣申請到的這塊內(nèi)存在代碼中再也沒有機(jī)會釋放掉了,這就是內(nèi)存泄漏。內(nèi)存泄漏是一類極為常見的問題,尤其對于不支持自動垃圾回收的語言來說,但并不是說自帶垃圾回收的語言像 Java 等就不會有內(nèi)存泄漏,這類語言同樣會遇到內(nèi)存泄漏問題。有內(nèi)存泄漏問題的程序會不斷的申請內(nèi)存,但不去釋放,這會導(dǎo)致進(jìn)程的堆區(qū)越來越大直到進(jìn)程被操作系統(tǒng) Kill 掉,在 Linux 系統(tǒng)中這就是有名的 OOM 機(jī)制,Out Of Memory Killer。

幸好,有專門的工具來檢測內(nèi)存泄漏出在了哪里,像valgrind、gperftools等。內(nèi)存泄漏是一個很有意思的問題,對于那些運(yùn)行時間很短的程序來說,內(nèi)存泄漏根本就不是事兒,因為對現(xiàn)代操作系統(tǒng)來說,進(jìn)程退出后操作系統(tǒng)回收其所有內(nèi)存,這就是意味著對于這類程序即使有內(nèi)存泄漏也就是發(fā)生在短時間內(nèi),甚至你根本就察覺不出來。但是對于服務(wù)器一類需要長時間運(yùn)行的程序來說內(nèi)存泄漏問題就比較嚴(yán)重了,內(nèi)存泄漏將會影響系統(tǒng)性能最終導(dǎo)致進(jìn)程被 OOM 殺掉,對于一些關(guān)鍵的程序來說,進(jìn)程退出就意味著收入損失,特別是在節(jié)假日等重要節(jié)點出現(xiàn)內(nèi)存泄漏的話,那么肯定又有一批程序員要被問責(zé)了。

 

引用已被釋放的內(nèi)存

  1. void add() { 
  2.   int* a = (int*)malloc(sizeof(int)); 
  3.   ... 
  4.   free(a); 
  5.   int* b = (int*)malloc(sizeof(int)); 
  6.   *b = *a; 

這段代碼在堆區(qū)申請了一塊內(nèi)存裝入整數(shù),之后釋放,可是在后續(xù)代碼中又再一次引用了被釋放的內(nèi)存塊,此時a指向的內(nèi)存保存什么內(nèi)容取決于malloc 內(nèi)部的工作狀態(tài):

指針a指向的那塊內(nèi)存釋放后沒有被 malloc 再次分配出去,那么此時a指向的值和之前一樣

指針a指向的那塊內(nèi)存已經(jīng)被 malloc分配出去了,此時a指向的內(nèi)存可能已經(jīng)被覆蓋,那么*b得到的就是一個被覆蓋掉的數(shù)據(jù),這類問題可能要等程序運(yùn)行很久才會發(fā)現(xiàn),而且往往難以定位。

 

循環(huán)遍歷是0開始的

  1. void init(int n) { 
  2.   int* arr = (int*)malloc(n * sizeof(int)); 
  3.   for (int i = 0; i <= n; i++) { 
  4.       arr[i] = i; 
  5.   } 

這段代碼的本意是要初始化數(shù)組,但忘記了數(shù)組遍歷是從 0 開始的,實際上述代碼執(zhí)行了 n+1 次賦值操作,同時將數(shù)組 arr 之后的內(nèi)存用 i 覆蓋掉了。這同樣取決于 malloc 的工作狀態(tài),如果 malloc 給到 arr 的內(nèi)存本身比n*sizeof(int)要大,那么覆蓋掉這塊內(nèi)存可能也不會有什么問題,但如果覆蓋的這塊內(nèi)存中保存有 malloc 用于維護(hù)內(nèi)存分配信息的話,那么此舉將破壞 malloc 的工作狀態(tài)。

 

指針大小與指針?biāo)赶驅(qū)ο蟮拇笮〔煌?/strong>

  1. int **create(int n) { 
  2. int i; 
  3. int **M = (int **)malloc(n * sizeof(int)); 
  4.  
  5. for (i = 0; i < n; i++) 
  6.   M[i] = (int *)malloc(m * sizeof(int)); 
  7.     
  8. return M; 

這段代碼的本意是要創(chuàng)建一個n*n二維數(shù)組,但其錯誤出現(xiàn)在了第3行,應(yīng)該是 sizeof(int *) 而不是sizeof(int),實際上這行代碼創(chuàng)建了一個包含有 n 個 int 的數(shù)組,而不是包含 n 個 int 指針的數(shù)組。但有趣的是,這行代碼在int和int*大小相同的系統(tǒng)上可以正常運(yùn)行,但是對于int指針比int要大的系統(tǒng)來說,上述代碼同樣會覆蓋掉數(shù)組M之后的一部分內(nèi)存,這里和上一個例子類似,如果這部分內(nèi)存是 malloc 用來保存內(nèi)存分配信息用的,那么也許當(dāng)釋放這段內(nèi)存時才會出現(xiàn)運(yùn)行時異常,此時可能已經(jīng)距離出現(xiàn)問題的那行代碼很遠(yuǎn)了,這類 bug 同樣難以排查。

棧緩沖器溢出

  1. void buffer_overflow() { 
  2. char buf[32]; 
  3.  
  4. gets(buf); 
  5. return

上面這段代碼總是假定用戶的輸入不過超過 32 字節(jié),一旦超過后,那么將立刻破壞棧幀中相鄰的數(shù)據(jù),破壞函數(shù)棧幀最好的結(jié)果是程序立刻crash,否則和前面的例子一樣,也許程序運(yùn)行很長一段時間后才出現(xiàn)錯誤,或者程序根本就不會有運(yùn)行時異常但是會給出錯誤的計算結(jié)果。實際上在上面幾個例子中也會有“溢出”,不過是在堆區(qū)上的溢出,但棧緩沖器溢出更容易導(dǎo)致問題,因為棧幀中保存有函數(shù)返回地址等重要信息,一類經(jīng)典的黑客攻擊技術(shù)就是利用棧緩沖區(qū)溢出,其原理也非常簡單。原來,每個函數(shù)運(yùn)行時在棧區(qū)都會存在一段棧幀,棧幀中保存有函數(shù)返回地址,在正常情況下,一個函數(shù)運(yùn)行完成后會根據(jù)棧幀中保存的返回地址跳轉(zhuǎn)到上一個函數(shù),假設(shè)函數(shù)A調(diào)用函數(shù)B,那么當(dāng)函數(shù)B運(yùn)行完成后就會返回函數(shù)A,這個過程如圖所示:

你可以在《函數(shù)運(yùn)行時在內(nèi)存中是什么樣子》這篇文章中找到關(guān)于函數(shù)運(yùn)行時棧幀的詳細(xì)講解。但如果代碼中存在棧緩沖區(qū)溢出問題,那么在黑客的精心設(shè)計下,溢出的部分會“恰好”覆蓋掉棧幀中的返回地址,將其修改為一個特定的地址,這個特定的地址中保存有黑客留下的惡意代碼,如圖所示:

這樣當(dāng)該進(jìn)程運(yùn)行起來后實際上是在執(zhí)行黑客的惡意代碼,這就是利用緩沖區(qū)溢出進(jìn)行攻擊的一個經(jīng)典案例。

 

操作指針?biāo)笇ο蠖侵羔槺旧?/strong>

  1. void delete_one(int** arr, intsize) { 
  2.   free(arr[*size - 1]); 
  3.   *size--; 

arr 是一個指針數(shù)組,這段代碼的本意是要刪除掉數(shù)組中最后一個元素,同時將數(shù)組的大小減一。但上述代碼的問題在于*和--有相同的優(yōu)先級,該代碼實際上會將 size 指針減1而不是把 size 指向的值減1。如果你足夠幸運(yùn)的話那么上述程序運(yùn)行到*size--時立刻 crash,這樣你就有機(jī)會快速發(fā)現(xiàn)問題。但更有可能的是上述代碼會看上去一切正常的繼續(xù)運(yùn)行并返回一個錯誤的執(zhí)行結(jié)果,這樣的bug排查起來會讓你終生難忘,因此當(dāng)不確定優(yōu)先級時不要吝嗇括號,加上它。

總結(jié)

 

內(nèi)存是計算機(jī)系統(tǒng)中至關(guān)重要的一個組成部分,C/C++這類偏底層的語言在帶來高性能的同事也帶來內(nèi)存相關(guān)的無盡問題,而這類問題通常難以排查,不過知彼知己,當(dāng)你理解了常見的內(nèi)存相關(guān)問題后將極大減少出現(xiàn)此類問題的概率。希望這篇文章對大家理解內(nèi)存與指針有所幫助。

本文轉(zhuǎn)載自微信公眾號「碼農(nóng)的荒島求生」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系碼農(nóng)的荒島求生公眾號。

 

責(zé)任編輯:武曉燕 來源: 碼農(nóng)的荒島求生
相關(guān)推薦

2022-03-08 13:08:45

數(shù)據(jù)庫異構(gòu)數(shù)據(jù)庫

2021-04-27 07:52:19

C++promisefuture

2015-05-27 09:50:17

碼農(nóng)程序員

2021-11-04 11:54:30

Linux內(nèi)存系統(tǒng)

2020-10-29 09:06:56

開發(fā)工具技術(shù)

2024-03-12 10:02:31

Python內(nèi)存編程

2014-11-21 10:46:56

Java開源項目

2019-05-16 09:50:39

負(fù)載均衡高可用數(shù)據(jù)

2022-03-21 14:09:19

面試C語言代碼

2021-03-22 16:55:14

Java程序員內(nèi)存

2019-10-18 12:57:38

邊緣計算云計算安全

2025-01-16 16:16:53

2012-02-20 11:33:29

Java

2019-11-22 09:30:59

設(shè)計Java程序員

2020-04-07 08:51:25

CCNP協(xié)議網(wǎng)絡(luò)協(xié)議路由

2023-12-15 10:42:05

2022-12-26 08:25:16

JavaScriptweb瀏覽器

2023-11-23 10:21:37

2019-08-27 08:02:03

Linux內(nèi)存占用命令

2019-11-24 23:12:30

Vim插件編輯器編程語言
點贊
收藏

51CTO技術(shù)棧公眾號