- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
6.5 Global Variables
如果之前的例子中的 x 变量不再是本地变量而是全局变量呢?那么就有机会接触任何指针,不仅仅是函数体,全局变量被认为 anti-pattern(通常被认为是一个不好的习惯),但是为了试验,我们可以这样做。
#!cpp
#include <stdio.h>
int x;
int main()
{
printf ("Enter X:
");
scanf ("%d", &x);
printf ("You entered %d...
", x);
return 0;
};
6.5.1 MSVC: x86
#!bash
_DATA SEGMENT
COMM _x:DWORD
$SG2456 DB ’Enter X:’, 0aH, 00H
$SG2457 DB ’%d’, 00H
$SG2458 DB ’You entered %d...’, 0aH, 00H
_DATA ENDS
PUBLIC _main
EXTRN _scanf:PROC
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2456
call _printf
add esp, 4
push OFFSET _x
push OFFSET $SG2457
call _scanf
add esp, 8
mov eax, DWORD PTR _x
push eax
push OFFSET $SG2458
call _printf
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
_TEXT ENDS
现在 x 变量被定义为在_DATA 部分,局部堆栈不允许再分配任何内存,除了直接访问内存所有通过栈的访问都不被允许。在执行的文件中全局变量还未初始化(实际上,我们为什么要在执行文件中为未初始化的变量分配一块?) 但是当访问这里时,系统会在这里分配一块 0 值。
现在让我们明白的来分配变量吧"
#!bash
int x=10; // default value
我们得到:
_DATA SEGMENT
_x DD 0aH
...
这里我们看见一个双字节的值 0xA(DD 表示双字节 = 32bit)
如果你在 IDA 中打开 compiled.exe,你会发现 x 变量被放置在_DATA 块的开始处,接着你就会看见文本字符串。
如果你在 IDA 中打开之前例子中的 compiled.exe 中 X 变量没有定义的地方,你就会看见像这样的东西:
#!bash
.data:0040FA80 _x dd ? ; DATA XREF: _main+10
.data:0040FA80 ; _main+22
.data:0040FA84 dword_40FA84 dd ? ; DATA XREF: _memset+1E
.data:0040FA84 ; unknown_libname_1+28
.data:0040FA88 dword_40FA88 dd ? ; DATA XREF: ___sbh_find_block+5
.data:0040FA88 ; ___sbh_free_block+2BC
.data:0040FA8C ; LPVOID lpMem
.data:0040FA8C lpMem dd ? ; DATA XREF: ___sbh_find_block+B
.data:0040FA8C ; ___sbh_free_block+2CA
.data:0040FA90 dword_40FA90 dd ? ; DATA XREF: _V6_HeapAlloc+13
.data:0040FA90 ; __calloc_impl+72
.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 OllyDbg: scanf() 执行后
表 6.6: OllyDbg 进程内存映射
6.5.3 GCC: x86
这和 linux 中几乎是一样的,除了 segment 的名称和属性:未初始化变量被放置在_bss 部分。
在 ELF 文件格式中,这部分数据有这样的属性:
; Segment type: Uninitialized
; Segment permissions: Read/Write
如果静态的分配一个值,比如 10,它将会被放在_data 部分,这部分有下面的属性:
; Segment type: Pure data
; Segment permissions: Read/Write
6.5.4 MSVC: x64
#!bash
_DATA SEGMENT
COMM x:DWORD
$SG2924 DB ’Enter X:’, 0aH, 00H
$SG2925 DB ’%d’, 00H
$SG2926 DB ’You entered %d...’, 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
main PROC
$LN3:
sub rsp, 40
lea rcx, OFFSET FLAT:$SG2924 ; ’Enter X:’
call printf
lea rdx, OFFSET FLAT:x
lea rcx, OFFSET FLAT:$SG2925 ; ’%d’
call scanf
mov edx, DWORD PTR x
lea rcx, OFFSET FLAT:$SG2926 ; ’You entered %d...’
call printf
; return 0
xor eax, eax
add rsp, 40
ret 0
main ENDP
_TEXT ENDS
几乎和 x86 中的代码是一样的,发现 x 变量的地址传递给 scanf() 用的是 LEA 指令,尽管第二处传递给 printf() 变量时用的是 MOV 指令,"DWORD PTR"——是汇编语言中的一部分(和机器码没有联系)。这就表示变量数据类型是 32-bit,于是 MOV 指令就被编码了。
6.5.5 ARM:Optimizing Keil + thumb mode
#!bash
.text:00000000 ; Segment type: Pure code
.text:00000000 AREA .text, CODE
...
.text:00000000 main
.text:00000000 PUSH {R4,LR}
.text:00000002 ADR R0, aEnterX ; "Enter X:
"
.text:00000004 BL __2printf
.text:00000008 LDR R1, =x
.text:0000000A ADR R0, aD ; "%d"
.text:0000000C BL __0scanf
.text:00000010 LDR R0, =x
.text:00000012 LDR R1, [R0]
.text:00000014 ADR R0, aYouEnteredD___ ; "You entered %d...
"
.text:00000016 BL __2printf
.text:0000001A MOVS R0, #0
.text:0000001C POP {R4,PC}
...
.text:00000020 aEnterX DCB "Enter X:",0xA,0 ; DATA XREF: main+2
.text:0000002A DCB 0
.text:0000002B DCB 0
.text:0000002C off_2C DCD x ; DATA XREF: main+8
.text:0000002C ; main+10
.text:00000030 aD DCB "%d",0 ; DATA XREF: main+A
.text:00000033 DCB 0
.text:00000034 aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF: main+14
.text:00000047 DCB 0
.text:00000047 ; .text ends
.text:00000047
...
.data:00000048 ; Segment type: Pure data
.data:00000048 AREA .data, DATA
.data:00000048 ; ORG 0x48
.data:00000048 EXPORT x
.data:00000048 x DCD 0xA ; DATA XREF: main+8
.data:00000048 ; main+10
.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 里了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论