X86汇编语言学习手记(3)
X86汇编语言学习手记(3)作者: BadcoffeeEmail: blog.oliver@gmail.com2004年12月原文出处:
http://blog.csdn.net/yayong
版权所有: 转载时请务必以超链接形式标明文章原始出处、作者信息及本声明
这是作者在学习X86汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。
严格说来,本篇文档所涉及到的内容并非局限于X86汇编领域,关于ELF文件格式、C语言、编译器及其它相关知识,还需参考相关文档。
作者会将反馈的错误修订后更新在自己的
Blog站点
上。
在X86汇编语言学习手记(1)(2)中,可以看到栈(Stack)作为进程执行过程中数据的临时存储区域,通常包含如下几类数据:
局部变量
函数调用的返回地址
函数调用的入口参数
SFP 栈框架指针 (可以通过编译器优化选项去除)
本章中,将继续通过实验,了解全局变量和静态变量在进程中是如何存储和分配的。
注:不同的Calling Convention对入口参数的规定是有一定差别的,函数调用入口参数也有可能通过寄存器来传递。
例如IBM的Power PC和AMD的Opteron,函数的入口参数全部或部分就是通过寄存器来传递的。
1. 全局变量和全局常量的实验
延续之前的方式,给出一个简单的C程序,其中声明的全局变量分为3种:
初始化过的全局变量
未初始化的全局变量
全局常量
#vi test5.c
int i=1;
int j=2;
int k=3;
int l,m;
int n;
const int o=7;
const int p=8;
const int q=9;
int main()
{
l=4;
m=5;
n=6;
return i+j+k+l+m+n+o+p+q;
}
# gcc test5.c -o test5
# mdb test5
Loading modules: [ libc.so.1 ]
> main::dis
main: pushl %ebp ; main至main+1,创建Stack Frame
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp ; main+3至main+0xe,为局部变量预留栈空间,并保证栈16字节对齐
main+0x10: movl $4,0x8060948 ; l=4
main+0x1a: movl $5,0x806094c ; m=5
main+0x24: movl $6,0x8060950 ; n=6
main+0x2e: movl 0x8060908,%eax
main+0x33: addl 0x8060904,%eax
main+0x39: addl 0x806090c,%eax
main+0x3f: addl 0x8060948,%eax
main+0x45: addl 0x806094c,%eax
main+0x4b: addl 0x8060950,%eax
main+0x51: addl 0x8050808,%eax
main+0x57: addl 0x805080c,%eax
main+0x5d: addl 0x8050810,%eax ; main+0x2e至main+0x5d,i+j+k+l+m+n+o+p+q
main+0x63: leave ; 撤销Stack Frame
main+0x64: ret ; main函数返回
现在,让我们在全局变量初始化后的地方设置断点,观察一下这几个全局变量的值:
> main+0x2e:b ; 设置断点
> :r ; 运行程序
mdb: stop at main+0x2e
mdb: target stopped at:
main+0x2e: movl 0x8060908,%eax
> 0x8060904,03/nap ; 察看全局变量 i,j,k的值
test5`i:
test5`i:
test5`i: 1
test5`j: 2
test5`k: 3
> 0x8060948,03/nap ; 察看全局变量l,m,n的值
test5`l:
test5`l:
test5`l: 4
test5`m: 5
test5`n: 6
> 0x8050808,03/nap ; 察看全局变量o,p,q的值
o:
o:
o: 7
p: 8
q: 9
>
概念:进程地址空间 Process Address Space
+----------------------+ ----> 0xFFFFFFFF (4GB)
| |
| Kernel Space |
| |
+----------------------+ ----> _kernel_base (0xE0000000)
| |
| Other Library |
: :
: :
| |
+----------------------+
| data section |
| Lib C Library |
| text section |
: :
: :
+----------------------+
| |
| |
: :
: grow up :
: :
| User Heap |
| |
+----------------------+
| bss |
| |
| User Data |
| |
+----------------------+
| |
| User Text |
| |
| |
+----------------------+ ----> 0x08050000
| |
| User Stack |
| |
: grow down :
: :
: :
| |
| |
+----------------------+ ----> 0
图 3-1 Solaris在IA32上的进程地址空间
如图3-1所示,Solaris在IA32上的进程地址空间和Linux是相似的,在用户进程的4GB地址空间内:
Kernel总是映射到用户地址空间的最高端,从宏定义_kernel_base至0xFFFFFFFF的区域
用户进程所依赖的各个共享库紧接着Kernel映射在用户地址空间的高端
最后是用户进程地址空间在地址空间的低端
各共享库的代码段,存放着二进制可执行的机器指令,是由kernel把该库ELF文件的代码段map到虚存空间,属性是read/exec/share
各共享库的数据段,存放着程序执行所需的全局变量,是由kernel把ELF文件的数据段map到虚存空间,属性为read/write/private
用户代码段,存放着二进制形式的可执行的机器指令,是由kernel把ELF文件的代码段map到虚存空间,属性为read/exec
用户代码段之上是数据段,存放着程序执行所需的全局变量,是由kernel把ELF文件的数据段map到虚存空间,属性为 read/write/private
用户代码段之下是栈(stack),作为进程的临时数据区,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
用户数据段之上是堆(heap),当且仅当malloc调用时存在,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
注意Stack和Heap的区别和联系:
相同点:
1. 都是来自于kernel分配的匿名内存,和磁盘上的ELF文件无关
2. 属性均为read/write/exec
不同点:
1.栈的分配在C语言层面一般是通过声明局部变量,调用函数引起的;堆的分配则是通过显式的调用(malloc)引起的
2.栈的释放在C语言层面是对用户透明的,用户不需要关心,由C编译器产生的相应的指令代劳;堆则需显式的调用(free)来释放
3.栈空间的增长方向是从高地址到低地址;堆空间的增长方向是由低地址到高地址
4.栈存在于任何进程的地址空间;堆则在程序中没有调用malloc的情况下不存在
用户地址空间的布局随着CPU和OS的不同,略有差异,以上都是基于X86 CPU在Solaris OS上的情况的讨论。
使用pmap命令,可以观察到系统中的指定进程的地址空间分布情况,下面就是用pmap观察bash进程的一个例子:
# pmap 1030
1030: -bash
08045000 12K rw--- [ stack ] ; bash的栈
08050000 444K r-x--/usr/bin/bash ; bash文本段
080CE000 72K rwx--/usr/bin/bash ; bash的数据段
080E0000 156K rwx-- [ heap ] ; bash的堆
DD8C0000 8K r-x--/usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2 ; 共享库的文本段
DD8D1000 4K rwx--/usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2 ; 共享库的数据段
DD8E0000 324K r-x--/usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
DD940000 8K rwx--/usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
DD950000 4K rwx-- [ anon ] ; 匿名内存, 由映射/dev/zero设备来创建的
DD960000 12K r-x--/usr/lib/libmp.so.2
DD973000 4K rwx--/usr/lib/libmp.so.2
DD980000 628K r-x--/usr/lib/libc.so.1
DDA2D000 24K rwx--/usr/lib/libc.so.1
DDA33000 4K rwx--/usr/lib/libc.so.1
DDA50000 4K rwx-- [ anon ]
DDA60000 548K r-x--/usr/lib/libnsl.so.1
DDAF9000 20K rwx--/usr/lib/libnsl.so.1
DDAFE000 32K rwx--/usr/lib/libnsl.so.1
DDB10000 44K r-x--/usr/lib/libsocket.so.1
DDB2B000 4K rwx--/usr/lib/libsocket.so.1
DDB30000 152K r-x--/usr/lib/libcurses.so.1
DDB66000 28K rwx--/usr/lib/libcurses.so.1
DDB6D000 8K rwx--/usr/lib/libcurses.so.1
DDB80000 4K r-x--/usr/lib/libdl.so.1
DDB90000 292K r-x--/usr/lib/ld.so.1
DDBE9000 16K rwx--/usr/lib/ld.so.1
DDBED000 8K rwx--/usr/lib/ld.so.1
total 2864K
问题:全局变量和全局常量在进程地址空间的位置?
显然,根据前面的叙述,全局变量在用户的数据段,那么全局常量呢,是数据段吗?
同样的,可以利用mdb将test5进程挂起,然后用pmap命令求证一下:
# mdb test5
Loading modules: [ libc.so.1 ]
> ::sysbp _exit ; 在系统调用_exit处设置断点
> :r ; 运行程序
mdb: stop on entry to _exit
mdb: target stopped at:
libc.so.1`exit+0x2b: jae +0x15
>
此时,程序运行后在_exit处挂起,可以利用pmap在另一个终端内查看test5进程的地址空间了:
# ps -ef | grep test5
root138713860 02:23:53 pts/1 0:00 test5
root139913900 02:25:03 pts/3 0:00 grep test5
root138613380 02:23:41 pts/1 0:00 mdb test5
# pmap -F 1387 ; 用pmap强制查看
1387: test5
08044000 16K rwx-- [ stack ] ; test5的stack
08050000 4K r-x--/export/home/asm/L3/test5 ; test5的代码段,起始地址为0x08050000
08060000 4K rwx--/export/home/asm/L3/test5 ; test5的数据段,起始地址为0x08060000
DDAC0000 628K r-x--/usr/lib/libc.so.1
DDB6D000 24K rwx--/usr/lib/libc.so.1
DDB73000 4K rwx--/usr/lib/libc.so.1
DDB80000 4K r-x--/usr/lib/libdl.so.1
DDB90000 292K r-x--/usr/lib/ld.so.1
DDBE9000 16K rwx--/usr/lib/ld.so.1
DDBED000 8K rwx--/usr/lib/ld.so.1
total 1000K
可以看到,由于test5程序没有使用malloc来申请内存,所以没有heap的映射
前面用mdb观察过这些全局变量和常量的初始化值,它们的地址分别是:
全局变量i,j,k: 0x8060904起始的12字节 全局变量l,m,n: 0x8060948起始的12字节 全局常量o,p,q: 0x8050808起始的12字节
显然,根据这些变量的地址,我们可以初步判断出这些变量属于哪个段:
由于test5数据段起始地址为0x08060000,我们得出结论:全局变量i,j,k,l,m,n属于数据段
而test5代码段的起始地址为0x08050000,我们得出结论:全局常量o,p,q属于代码段
得出这个结论的确有点让人意外:全局常量竟然在代码段。
却又似乎在情理之中:数据段内存映射后的属性是r/w/x,而常量要求是只读属性,所以在代码段(r-x)就合情合理了。
问题:为什么这些全局变量地址不是连续的?
很容易注意到,全局变量i,j,k和l,m,n以及全局常量o,p,q是连续声明的,但地址实际上并不连续,而是在3段连续12字节的地址上。
当然,全局常量属于代码段,所以地址和全局变量是分开的;那么,为什么全局变量也并非连续呢?
前面谈到数据段实际上是从ELF格式的二进制文件映射到进程的地址空间的,就通过分析ELF文件格式来寻找答案吧:
# file test5
test5: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
# elfdump test5
ELF Header ; ELF头信息共52(0x34)字节,具体意义可以参考ELF format的相关文档
ei_magic: { 0x7f, E, L, F } ; ELF的幻数
ei_class: ELFCLASS32 ei_data: ELFDATA2LSB ; 32位的ELF文件,小端(LSB)编码
e_machine:EM_386 e_version: EV_CURRENT ; Intel 80386, 版本1
e_type: ET_EXEC ; 可执行文件
e_flags: 0
e_entry: 0x8050600e_ehsize: 52e_shstrndx: 27 ; 程序入口点_start的地址0x8050600
e_shoff: 0x1584e_shentsize:40e_shnum: 29 ; Section header table的大小是29*40
e_phoff: 0x34e_phentsize:32e_phnum: 5
Program Header: ; 描述Program header table本身在内存中如何映射
p_vaddr: 0x8050034 p_flags: [ PF_XPF_R ]
p_paddr: 0 p_type: [ PT_PHDR ]
p_filesz: 0xa0 p_memsz: 0xa0
p_offset: 0x34 p_align: 0
Program Header: ; 描述程序装载器的路径名(.interp section)存放在文件的位置
p_vaddr: 0 p_flags: [ PF_R ]
p_paddr: 0 p_type: [ PT_INTERP ]
p_filesz: 0x11 p_memsz: 0
p_offset: 0xd4 p_align: 0
Program Header: ; 描述代码段在内存中如何映射,起始地址0x8050000,大小为 0x814
p_vaddr: 0x8050000 p_flags: [ PF_XPF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x814 p_memsz: 0x814
p_offset: 0 p_align: 0x10000
Program Header: ; 描述数据段在内存中如何映射,起始地址0x8060814,大小为0x144
p_vaddr: 0x8060814 p_flags: [ PF_XPF_WPF_R ]
p_paddr: 0 p_type: [ PT_LOAD ]
p_filesz: 0x118 p_memsz: 0x144
p_offset: 0x814 p_align: 0x10000
Program Header: ; 描述动态链接信息(.dynamic section)在内存中如何映射
p_vaddr: 0x8060848 p_flags: [ PF_XPF_WPF_R ]
p_paddr: 0 p_type: [ PT_DYNAMIC ]
p_filesz: 0xb8 p_memsz: 0
p_offset: 0x848 p_align: 0
Section Header:sh_name: .interp; 该section保存了程序的解释程序(interpreter)的路径
sh_addr: 0x80500d4 sh_flags: [ SHF_ALLOC ]
sh_size: 0x11 sh_type: [ SHT_PROGBITS ]
sh_offset: 0xd4 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header:sh_name: .hash ; 该section保存着一个符号的哈希表
sh_addr: 0x80500e8 sh_flags: [ SHF_ALLOC ]
sh_size: 0x104 sh_type: [ SHT_HASH ]
sh_offset: 0xe8 sh_entsize: 0x4
sh_link: 3 sh_info: 0
sh_addralign: 0x4
Section Header:sh_name: .dynsym ; 该section保存着动态符号表
sh_addr: 0x80501ec sh_flags: [ SHF_ALLOC ]
sh_size: 0x200 sh_type: [ SHT_DYNSYM ]
sh_offset: 0x1ec sh_entsize: 0x10
sh_link: 4 sh_info: 1
sh_addralign: 0x4
Section Header:sh_name: .dynstr ; 该section保存着动态连接时需要的字符串
sh_addr: 0x80503ec sh_flags: [ SHF_ALLOCSHF_STRINGS ]
sh_size: 0x11a sh_type: [ SHT_STRTAB ]
sh_offset: 0x3ec sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x1
Section Header:sh_name: .SUNW_version ; 该section是SUN扩展的,保存版本信息
sh_addr: 0x8050508 sh_flags: [ SHF_ALLOC ]
sh_size: 0x20 sh_type: [ SHT_SUNW_verneed ]
sh_offset: 0x508 sh_entsize: 0
sh_link: 4 sh_info: 1
sh_addralign: 0x4
Section Header:sh_name: .rel.got ; 该section保存着.got section中部分符号的重定位信息
sh_addr: 0x8050528 sh_flags: [ SHF_ALLOCSHF_INFO_LINK ]
sh_size: 0x18 sh_type: [ SHT_REL ]
sh_offset: 0x528 sh_entsize: 0x8
sh_link: 3 sh_info: 14
sh_addralign: 0x4
Section Header:sh_name: .rel.bss ; 该section保存着.bss section中部分符号的重定位信息
sh_addr: 0x8050540 sh_flags: [ SHF_ALLOCSHF_INFO_LINK ]
sh_size: 0x8 sh_type: [ SHT_REL ]
sh_offset: 0x540 sh_entsize: 0x8
sh_link: 3 sh_info: 22
sh_addralign: 0x4
Section Header:sh_name: .rel.plt ; 该section保存着.plt section中部分符号的重定位信息
sh_addr: 0x8050548 sh_flags: [ SHF_ALLOCSHF_INFO_LINK ]
sh_size: 0x38 sh_type: [ SHT_REL ]
sh_offset: 0x548 sh_entsize: 0x8
sh_link: 3 sh_info: 9
sh_addralign: 0x4
Section Header:sh_name: .plt ; 该section保存着过程连接表(Procedure Linkage Table)
sh_addr: 0x8050580 sh_flags: [ SHF_ALLOCSHF_EXECINSTR ]
sh_size: 0x80 sh_type: [ SHT_PROGBITS ]
sh_offset: 0x580 sh_entsize: 0x10
sh_link: 0 sh_info: 0
sh_addralign: 0x4
Section Header:sh_name: .text ; 该section保存着程序的正文部分,即可执行指令
sh_addr: 0x8050600 sh_flags: [ SHF_ALLOCSHF_EXECINSTR ]
sh_size: 0x1ec sh_type: [ SHT_PROGBITS ]
sh_offset: 0x600 sh_entsize: 0
sh_link: 0 sh_info: 0
sh_addralign: 0x4
........由于CU blog的问题,本文超过64K,因此看全文请到
http://blog.csdn.net/yayong
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/768/showart_8814.html 回贴支持。谢谢:) 回贴支持。谢谢:)
页:
[1]