2009年10月30日 星期五

唯讀字串


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   ENDS
CONST 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:
array 機器碼:66 A1 78 89 42 00 66 89 45 E0 pointer 機器碼:C7 45 D4 78 89 42 00
但使用上能以較快的微指令來實作 (lea 一般占 1-cycle,
可勝出大部分 mov 家族) 來實作,故速度較快。

沒有留言:

張貼留言