總結_判斷指定地址內存空間合法

判斷指定地址空間合法之總結


前題:之前一直在糾結,怎麼樣在代碼中判斷指定邏輯地址的內存空間可以被正常讀寫,或許有一個API接口來判斷我們來判斷這個事情。但是很遺憾的是,很長時間都沒有找到解決方法。最近在重新把它拿起來好好分析一下,寫一下關於這個問題的自己一點總結。


現狀:當一次程序來讀取一個非法的內存地址的時候,程序將會報出一下異常segment fault(段錯誤),然後異常掉整個進程。如果我們想判讀一個地址是否可以被讀寫,那麼我們就要試一下讀寫這個地址看會不會異常,或者看這個邏輯地址對應的物理地址所在內存頁的屬性,因爲我們這裏主要討論的是用戶態,希望不要討論其他的東西。如果這個地址合法,那麼讀寫當然很正常,並且很高興的告訴你,進程還活着。如果這個地址非法呢?那麼這個進程將會收到一個異常,並死掉。這是一個賭局,非死即活。當然我們不希望這樣,判斷一個地址是否合法,把整個進程的小命都打上,那麼我們如何能夠判斷當前地址合法,並保證判斷成功之後,我這個進程還活着。我想到了一個方法,舉一個列子,這裏把未知指針指向的空間當作一個坑,父進程告訴我,我要看看這個坑我能不能過去,那麼他就找了一個替身,然後讓這個替身過去踩兩下,然後回來告訴我,這個坑我能不能過去。


方法:這裏我使用父進程創建一個子進程,利用進程之間的特性,子進程將會拷貝所有父進程中的所有空間,除了代碼空間。那麼可以肯定的是,子進程和父進程所使用的內存空間在物理內存肯定是不一樣的,那麼爲什麼能夠使用子進程來代替父進程判斷這個指針指向的空間是否是合法的呢?雖然說子進程和父進程之間所使用的內存空間並不相同,但是子進程的空間結構是繼承父進程的,可以看作子進程和父進程邏輯數據空間一樣。如果子進程訪問這個指針指向的地址異常(收到一個SIGSEGV信號),那麼將響應子進程之前註冊的SIGSEGV信號處理動作,子進程將發送一個信號給他的父進程,並終結掉自己。如果父進程收到這個信號,那麼可以判斷,這個指針指向的內存空間有問題,不能夠使用。如果子進程訪問這個指針活下來了,那麼子進程同樣會告訴父進程,我活下來了。然後父進程就知道這個地址空間是否合法了。


測試代碼:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

typedef void (*sighandler_t)(int);


int memory_is_ok(uint8_t *addr)
{
	void child_signal(int signo)
	{
		switch (signo)
		{
			case 6:
				printf ("the addr is bad\n");
				break;
			case 7:
				printf ("the addr is ok\n");
				break;
			default:
				printf ("something will be wrong\n");
		}	
	}

	void signal_process(int signo)
	{
		printf ("child get signo:%d\n", signo);
		if (signo == 6)
			return ;
		pid_t ppid = getppid();
		kill( ppid, 6);
		exit(0);
	}

	pid_t cur = fork();
	if (cur == 0){
		signal(SIGSEGV, signal_process);	
		*addr = 0;
		pid_t ppid = getppid();
		kill( ppid, 7);
		exit(0);
	}else {
		signal( 6, child_signal);	
		signal(7, child_signal);
		wait(NULL);
	}
}



int main(void)
{
	uint8_t *pos = malloc (1024);

	int i = 0;
	for (i = 0; i < 4096;i++)
	memory_is_ok(pos + 1024 + i);


	return 0;
}


結果:測試輸入NULL空地址的時候,和預期的一樣告訴你這個地址不可訪問。輸入一個合法地址發現,和預期一樣反饋這個地址合法。但是當我輸入一個其他偏移地址的時候,發現這個地址不在申請範圍空間內,但是依舊告訴我,這個地址可用。

瞬間凌亂了。難道是字節對齊?當我把偏移送到4K的時候發現,依舊可以,那麼就不是字節對齊的問題。那麼這塊空間有效化的階段應該是代碼編譯,運行時系統直接將所有段分配好,這裏這個指針指向地址真好在這個初始分配空間範圍內,在這個範圍內的所有數據都是有效合法的。




這裏就要科普一下進程內存結構,當程序在編譯之後,編譯器將會按照程序中的結構,將整個空間分配爲代碼段,全局且初始化段,全局且未初始化段或初始化值爲0段,可生長的堆段,可生長的棧段,以及分配給內核運行的1G空間。當編寫完代碼,並編譯之後,可以使用size 指令來查看當前可執行程序編譯器分配空間。

示例:size main

   text   data    bss    dec    hex filename
   2166    584   1056   3806    ede main

如果在data字段中放了一個變量,bss字段放了一個變量;之後在data地址的基礎上偏移腳伸到BSS段,覆蓋寫將會擦掉BSS段內數據,但是不會報錯。但是。。。。,如果你的腳伸到堆哪兒,他會報錯嗎?我調試了一個程序。

全局變量:

uint8_t data[8] = {0x01} ;
uint8_t bss[2024] ;

main:

int main(void)
{
  int i = 0;
  uint8_t *pos = malloc (1);
  for(i = 0; i < 4500; i++)
    *(data + i) = 0x03;
    //memory_is_ok(pos + 8 + i);


  return 0;
}


size 編譯好的程序看一下:

   text   data    bss    dec    hex filename
   2275    592   2056   4923   133b main


直接運行,發現程序報錯; 當前指針指向Data字段, 偏移總長應該是data_len + bss_len;那麼當前偏移4500絕對是滿足兩個字長,到了堆空間了。這時候,我調試一下偏移長度,設置爲4000;

結果:程序沒有報任何錯誤,就這樣跑完了。

分析:但是這是和我們期望的結果是不一樣的,4000字長應該是滿足data_len + bss_len,但是沒有報錯,難道是棧空間變了,但是程序中,我並沒有修改其他地方,只是修改了偏移字長而已。所以棧空間最起碼應該是保持不變。奇怪了,那麼現在唯一能夠懷疑的是data_len和bss_len的空間是需要自動對齊的。這裏假設data_len + bss_len > 4000 && < 8000;

全局變量:

uint8_t data[8] = {0x01} ;
uint8_t bss[4096] ;

偏移設置:8000

結果:程序沒有報錯;偏移設置其他值發現最接近8K;


結論:data字段中的數據是存放在.o目標文件中,而bss字段的數據是運行的時候放在內存中的,size計算出來的只有運行狀態時bss字段大小。而且bss字段大小,應該滿足4K字節頁大小存放。參考[3]

所以,不管怎麼說,當data字段的指針腿超出bss字段的時候,就開始報錯了。實際上這時候堆空間還沒有申請,所以這時候一定開始報錯。


最後總結:本來想針對於做一個API接口來做一個內存訪問探針,來確定指定空間是否合法訪問,沒想到扯出來這麼多。根據一開始提到的,這裏可以做這樣的一個總結,要想判斷一個地址是否可以讀寫,那麼就要看這個地址在什麼位置,是否在合法空間範圍內。呵呵,好像繞回來了。


最後,調試時候還是使用GDB放心,指向的指針不對了,直接會告訴你這個地址不可達。



參考文檔:

[1]、http://blog.csdn.net/theone10211024/article/details/13774669

[2]、http://www.cnblogs.com/fengyv/p/3789252.html

[3]、 http://blog.csdn.net/sunnybeike/article/details/8003162

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章