char *p = "hello world!"; p[0] = 'H';此段程式首先將 p 指向一個唯讀字串 (const char [13]) 的起始位址,
此字串在 VC++ Debug Mode 下,位於記憶體的唯讀區段中,如下所示:
(在 Release Mode 下,某些版本會放在可讀寫的 .DATA 中)
.686P .XMM .model flat CONST SEGMENT $SG18132 DB 'hello world!', 00H ;<-----位於此處 CONST ENDS _TEXT SEGMENT _main PROC push ebp mov ebp, esp push ecx mov DWORD PTR _p$[ebp], OFFSET $SG18132 mov eax, DWORD PTR _p$[ebp] mov BYTE PTR [eax], 72 ;'H' xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP _TEXT ENDSCONST SEGMENT 對應到的 Page Table Entry,其 R/W 旗標設為 0,
使索引到的內容可讀不可寫,一旦指令變更此段記憶體的內容,
會發生存取違規。
若是用 g++ 編,則會置於唯讀 .rdata 區段
.section .rdata,"dr" LC0: .ascii "hello world!\0" .text .align 2 .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp subl $8, %esp #播出一塊空間放 p movl $LC0, -4(%ebp) #p = "hello world!" movl -4(%ebp), %eax #eax = p movb $72, (%eax) #p[0] = 'H' movl $0, %eax leave ret 改成 char p[] = "hello world!"; 或 char p[] = {"hello world!"}; 後, 程式分配 16Bytes 的堆疊空間給 p[], 並將 "hello world!" 由常數區複印一份到此空間中。 此堆疊區段可讀、可寫、可執行,故之後 p[0]='H' 時, 便不會因寫入常數區段而發生存取違規。 _TEXT SEGMENT _p$ = -16 ; size = 13, 對齊 4k 邊界後為 16 _main PROC push ebp mov ebp, esp sub esp, 16 ;挪出可容納 13 Byte 的堆疊空間 mov eax, DWORD PTR $SG18132 ;將 eax 指向 $SG18132 開始拷貝字串 mov DWORD PTR _p$[ebp], eax ;拷貝hell mov ecx, DWORD PTR $SG18132+4 mov DWORD PTR _p$[ebp+4], ecx ;拷貝o wo mov edx, DWORD PTR $SG18132+8 mov DWORD PTR _p$[ebp+8], edx ;拷貝rld! mov al, BYTE PTR $SG18132+12 mov BYTE PTR _p$[ebp+12], al ;拷貝\0 mov BYTE PTR _p$[ebp], 72 ;令 p[0] = 'H' xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP _TEXT ENDS
g++ 下,亦生成類似的拷貝動作,只是拷貝方向不同:
_main: pushl %ebp movl %esp, %ebp subl $40, %esp movl LC0, %eax movl %eax, -24(%ebp) movl LC0+4, %eax movl %eax, -20(%ebp) movl LC0+8, %eax movl %eax, -16(%ebp) movzbl LC0+12, %eax movb %al, -12(%ebp) movb $72, -24(%ebp) movl $0, %eax leave ret
欲在 Intel 保護模式下存取唯讀區段,可建立一個 segment descriptor,
將第 9 bit 置為 1,再指向該區段,進行分頁存取。
Linux 下可使用 mprotect:
#include <sys/mman.h> #include <limits.h> ..... if (-1 != mprotect (p, strlen(p), PROT_WRITE)) p[0] = 'H';Win32 下可使用 VirtualProtect:
#include <windows.h> ..... DWORD oldFlag; if (VirtualProtect (p, strlen(p), PAGE_READWRITE, &oldFlag)) p[0] = 'H';
再來比較 "\0" 與 '\0':
\x 是指將 x 映射成 ASCII 或其他指定碼表中索引為 x 的字元。
'\0' 一般被編譯成 .asm 中的常數定字,成為組語指令的「一部份」。
"\0" 則佔有 2 個字元碼寬,位於 Stack 常數保護區中,
其 asm code 視被 assign 的對象以及編譯器類型而有所不同,
Visual Studio 2008 實作方法為:
char b; char ch = '0'; //mov byte ptr [ch],30h char a[] = "\0"; //mov ax,word ptr ["\0" (428978h)] //mov word ptr [a], ax char *p = "\0"; //mov dword ptr [p], offset "\0" (428978h) char *c; c = a; //lea eax,[a] //mov dword ptr [c],eax c = p; //mov eax,dword ptr [p] //mov dword ptr [c],eax b = ch; //mov al,byte ptr [ch] //mov byte ptr ,al
'\0' 成為定字 0x30,可直接編成機器碼:C6 45 EF 30
此處,array 的配置較 pointer 繁瑣,多了 3 Byte:
但使用上能以較快的微指令來實作 (lea 一般占 1-cycle,array 機器碼:66 A1 78 89 42 00 66 89 45 E0 pointer 機器碼:C7 45 D4 78 89 42 00
可勝出大部分 mov 家族) 來實作,故速度較快。
沒有留言:
張貼留言