6.5 Global Variables

如果之前的例子中的x变量不再是本地变量而是全局变量呢?那么就有机会接触任何指针,不仅仅是函数体,全局变量被认为anti-pattern(通常被认为是一个不好的习惯),但是为了试验,我们可以这样做。

  1. #include <stdio.h>
  2. int x;
  3. int main()
  4. {
  5. printf ("Enter X:
  6. ");
  7. scanf ("%d", &x);
  8. printf ("You entered %d...
  9. ", x);
  10. return 0;
  11. };

6.5.1 MSVC: x86

  1. _DATA SEGMENT
  2. COMM _x:DWORD
  3. $SG2456 DB Enter X:’, 0aH, 00H
  4. $SG2457 DB ’%d’, 00H
  5. $SG2458 DB You entered %d...’, 0aH, 00H
  6. _DATA ENDS
  7. PUBLIC _main
  8. EXTRN _scanf:PROC
  9. EXTRN _printf:PROC
  10. ; Function compile flags: /Odtp
  11. _TEXT SEGMENT
  12. _main PROC
  13. push ebp
  14. mov ebp, esp
  15. push OFFSET $SG2456
  16. call _printf
  17. add esp, 4
  18. push OFFSET _x
  19. push OFFSET $SG2457
  20. call _scanf
  21. add esp, 8
  22. mov eax, DWORD PTR _x
  23. push eax
  24. push OFFSET $SG2458
  25. call _printf
  26. add esp, 8
  27. xor eax, eax
  28. pop ebp
  29. ret 0
  30. _main ENDP
  31. _TEXT ENDS

现在x变量被定义为在_DATA部分,局部堆栈不允许再分配任何内存,除了直接访问内存所有通过栈的访问都不被允许。在执行的文件中全局变量还未初始化(实际上,我们为什么要在执行文件中为未初始化的变量分配一块?)但是当访问这里时,系统会在这里分配一块0值。

现在让我们来分析变量的分配。

int x=10; // default value

我们得到:

  1. _DATA SEGMENT
  2. _x DD 0aH
  3. ...

这里我们看见一个双字节的值0xA(DD 表示双字节 = 32bit)

如果你在IDA中打开compiled.exe,你会发现x变量被放置在_DATA块的开始处,接着你就会看见文本字符串。

如果你在IDA中打开之前例子中的compiled.exe中X变量没有定义的地方,你就会看见像这样的东西:

  1. .data:0040FA80 _x dd ? ; DATA XREF: _main+10
  2. .data:0040FA80 ; _main+22
  3. .data:0040FA84 dword_40FA84 dd ? ; DATA XREF: _memset+1E
  4. .data:0040FA84 ; unknown_libname_1+28
  5. .data:0040FA88 dword_40FA88 dd ? ; DATA XREF: ___sbh_find_block+5
  6. .data:0040FA88 ; ___sbh_free_block+2BC
  7. .data:0040FA8C ; LPVOID lpMem
  8. .data:0040FA8C lpMem dd ? ; DATA XREF: ___sbh_find_block+B
  9. .data:0040FA8C ; ___sbh_free_block+2CA
  10. .data:0040FA90 dword_40FA90 dd ? ; DATA XREF: _V6_HeapAlloc+13
  11. .data:0040FA90 ; __calloc_impl+72
  12. .data:0040FA94 dword_40FA94 dd ? ; DATA XREF: ___sbh_free_block+2FE

被_x替换了?其它变量也并未要求初始化,这也就是说在载入exe至内存后,在这里有一块针对所有变量的空间,并且还有一些随机的垃圾数据。但在在exe中这些没有初始化的变量并不影响什么,比如它适合大数组。

6.5.2 MSVC: x86 + OllyDbg

到这里事情就变得简单了(见表6.5),变量都在data部分,顺便说一句,在PUSH指令后,压入x的地址,被执行后,地址将会在栈中显示,那么右击元组数据,点击“Fllow in dump”,然后变量就会在左侧内存窗口显示.

在命令行窗口中输入123后,这里就会显示0x7B

但是为什么第一个字节是7B?合理的猜测,这里会有一组00 00 7B,被称为是字节顺序,然后在x86中使用的是小端,也就是说低位数据先写,高位数据后写。

不一会,这里的32-bit值就会载入到EAX中,然后被传递给printf().

X变量地址是0xDC3390.在OllyDbg中我们看进程内存映射(Alt-M),然后发现这个地在PE文件.data结构处。见表6.6

6.5 Global Variables - 图1

表6.5 OllyDbg: scanf()执行后

6.5 Global Variables - 图2

表6.6: OllyDbg 进程内存映射

6.5.3 GCC: x86

这和linux中几乎是一样的,除了segment的名称和属性:未初始化变量被放置在_bss部分。

在ELF文件格式中,这部分数据有这样的属性:

  1. ; Segment type: Uninitialized
  2. ; Segment permissions: Read/Write

如果静态的分配一个值,比如10,它将会被放在_data部分,这部分有下面的属性:

  1. ; Segment type: Pure data
  2. ; Segment permissions: Read/Write

6.5.4 MSVC: x64

  1. _DATA SEGMENT
  2. COMM x:DWORD
  3. $SG2924 DB Enter X:’, 0aH, 00H
  4. $SG2925 DB ’%d’, 00H
  5. $SG2926 DB You entered %d...’, 0aH, 00H
  6. _DATA ENDS
  7. _TEXT SEGMENT
  8. main PROC
  9. $LN3:
  10. sub rsp, 40
  11. lea rcx, OFFSET FLAT:$SG2924 ; Enter X:’
  12. call printf
  13. lea rdx, OFFSET FLAT:x
  14. lea rcx, OFFSET FLAT:$SG2925 ; ’%d
  15. call scanf
  16. mov edx, DWORD PTR x
  17. lea rcx, OFFSET FLAT:$SG2926 ; You entered %d...’
  18. call printf
  19. ; return 0
  20. xor eax, eax
  21. add rsp, 40
  22. ret 0
  23. main ENDP
  24. _TEXT ENDS

几乎和x86中的代码是一样的,发现x变量的地址传递给scanf()用的是LEA指令,尽管第二处传递给printf()变量时用的是MOV指令,“DWORD PTR”——是汇编语言中的一部分(和机器码没有联系)。这就表示变量数据类型是32-bit,于是MOV指令就被编码了。

6.5.5 ARM:Optimizing Keil + thumb mode

  1. .text:00000000 ; Segment type: Pure code
  2. .text:00000000 AREA .text, CODE
  3. ...
  4. .text:00000000 main
  5. .text:00000000 PUSH {R4,LR}
  6. .text:00000002 ADR R0, aEnterX ; "Enter X:
  7. "
  8. .text:00000004 BL __2printf
  9. .text:00000008 LDR R1, =x
  10. .text:0000000A ADR R0, aD ; "%d"
  11. .text:0000000C BL __0scanf
  12. .text:00000010 LDR R0, =x
  13. .text:00000012 LDR R1, [R0]
  14. .text:00000014 ADR R0, aYouEnteredD___ ; "You entered %d...
  15. "
  16. .text:00000016 BL __2printf
  17. .text:0000001A MOVS R0, #0
  18. .text:0000001C POP {R4,PC}
  19. ...
  20. .text:00000020 aEnterX DCB "Enter X:",0xA,0 ; DATA XREF: main+2
  21. .text:0000002A DCB 0
  22. .text:0000002B DCB 0
  23. .text:0000002C off_2C DCD x ; DATA XREF: main+8
  24. .text:0000002C ; main+10
  25. .text:00000030 aD DCB "%d",0 ; DATA XREF: main+A
  26. .text:00000033 DCB 0
  27. .text:00000034 aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF: main+14
  28. .text:00000047 DCB 0
  29. .text:00000047 ; .text ends
  30. .text:00000047
  31. ...
  32. .data:00000048 ; Segment type: Pure data
  33. .data:00000048 AREA .data, DATA
  34. .data:00000048 ; ORG 0x48
  35. .data:00000048 EXPORT x
  36. .data:00000048 x DCD 0xA ; DATA XREF: main+8
  37. .data:00000048 ; main+10
  38. .data:00000048 ; .data ends

那么,现在x变量以某种方式变为全局的,现在被放置在另一个部分中。命名为data块(.data)。有人可能会问,为什么文本字符串被放在了代码块(.text),而且x可以被放在这?因为这是变量,而且根据它的定义,它可以变化,也有可能会频繁变化,不频繁变化的代码块可以被放置在ROM中,变化的变量在RAM中,当有ROM时在RAM中储存不变的变量是不利于节约资源的。

此外,RAM中数据部分常量必须在之前初始化,因为在RAM使用后,很明显,将会包含杂乱的信息。

继续向前,我们可以看到,在代码片段,有个指针指向X变量(0ff_2C)。然后所有关于变量的操作都是通过这个指针。这也是x变量可以被放在远离这里地方的原因。所以他的地址一定被存在离这很近的地方。LDR指令在thumb模式下只可访问指向地址在1020bytes内的数据。同样的指令在ARM模式下——范围就达到了4095bytes,也就是x变量地址一定要在这附近的原因。因为没法保证链接时会把这个变量放在附近。

另外,如果变量以const声明,Keil编译环境下则会将变量放在.constdata部分,大概从那以后,链接时就可以把这部分和代码块放在ROM里了。