0x0040 0000基地址


转自:http://www.newsmth.net/nForum/#!article/CSArch/51826


在mips体系结构里,mips为编译后的程序分配内存(经过编译、汇编、链接),大致如下: 

栈指针初始化为:0x7fff fffc, 
代码段起始地址:0x0040 0000, 
静态数据:0x1000 0000 ~ 0x1000 ffff 

 

共享内存段
自由存储区(堆)
BSS段
数据段
只读数据段                         
代码段              

对于32位系统虚拟地址空间被抽象为编号0~0xffff ffff的字节序列,它是平坦的,线性的,被系统抽象了的,所以叫它平坦地址或线性地址、虚拟地址。

对于Linux来说,保留高1G为系统使用。0-3G空间被应用程序也就是进程独占。


转自:http://tieba.baidu.com/p/1004082362


声明:
写给新手的!关于这个我相信有的老鸟也不一定那么清楚,所以老鸟亦可作为参考!

看时间决定~先写下题目!然后能写多少写多少!因为是深夜我睡没着闲,所以不知道多久睡意挡不住就睡觉去了,建议各位兄弟姐妹在我说回帖允许之前不要回帖,以保证整篇文章的清晰性~因为我基本上要做一个对新手来说类似教程级的篇文章,所以完整性很重要!

为了不让自己没稿子满嘴跑火车,先定下如下大纲:
1.80X86 32位汇编基础以及寄存器设定
2.栈帧与C函数调用
3.函数调用的汇编级解释以及栈图
4.stdcall和cdcel

那么开始了!

1.80X86 32位CPU的编程模型programming model

80X86有16个通用寄存器register。从某种程度上来说介绍80X86的CPU编程模型,就是介绍这16个寄存器。没听过CPU寄存器这名字的童鞋不用看下去了请回家睡觉去。本文基本上适合大抵知道汇编怎么回事的童鞋而不是完全的汇编白痴。
另外,介绍的是32位汇编,请把80X86的16位汇编先忘记掉,这么短的文章不可能介绍完汇编,而是给出汇编最基本的东西和最简单的抽象。可能的话稍微解释下16位汇编。

eax ebx ecx edx
这4个寄存器是通用寄存器。用来暂存数据的地方。

esi edi(extension source index, extension destination index.)
它们也可以用来暂存数据。更一般的是伴随串指令使用。

esp ebp(extension stack pointer, extension base pointer)
栈指针寄存器和基址指针寄存器。关于栈和过程调用,最重要的寄存器就是这两个了!绝对不要忘掉这两个!

eip(extension instruction pointer, or program count)
指令指针寄存器!这就是“顺序存储控制”的核心!又称程序计数器!

eflag
标志寄存器。算术、逻辑及相关指令运算会影响该标志寄存器中的位。这个寄存器很重要也很麻烦。

以上!就是32位汇编(又称平坦地址模式汇编)会使用到的所有寄存器,一共十个,都是32位的。啊不是说十六个寄存器吗?

对,还有6个寄存器,分别名为:
cs ;代码段寄存器code segment
ds ;数据段寄存器data segment
ss ;栈寄存器stack segment
es fs gs; 附加段寄存器
这6个寄存器都是16位寄存器。即使是现今的80686 32位系统中,它们仍然是16位的。这些段寄存器在8086中用来对内存地址进行段指定。有8086 16位汇编知识的同学都知道怎么回事,...还是解释一下吧,8086是16位CPU,而地址线是20位。20根地址线表明能寻址的空间是2^20也就是1M(1024 * 1024).16位不够表达1M的地址空间,因此由“段*16+偏移”得到内存地址值。
但是在32位系统中,这些段寄存器已经不怎么使用了。总之32位汇编不需要关注这些寄存器,因为32位系统CPU和各寄存器是32位,地址线也是32位,一个32位值足够表达32位寻址空间。...实际上这些段寄存器在32位系统中是同一个值,用来指向某个索引表,但这是本文不需要在此关注的东西。

以上,16个寄存器介绍完毕!接下来介绍简洁的编程模型抽象!

  • 2楼
  • 2011-02-18 02:48
由于是简单而本质的抽象,因此我们不考虑分页机制、MMU(memory management unit)之类的。正是如此,它们本来对于我们就是透明的。

所以内存就被考虑为一个从编号(地址)0开始、以编号(地址)0xffff ffff结束的字节序列。每一个字节都被顺序地编号。编号就是字节的地址。
在32位FLAT模式汇编中,本来就是如此。

在程序加载入内存后,程序的指令和数据都按某种方式存放在内存里面。要访问和执行他们,只需要知道他们的地址就可以了。

最重要的东西登场,它就是eip,指令指针寄存器,或称程序计数器。eip中的值程序员无法修改(嗯,可是汇编程序员呢?汇编程序员也无法修改它的值吗?废话,汇编程序员也是程序员啊!),它的值就是下一条即将执行的指令的地址。就是说eip永远指向下一条指令。

然后就是esp,它指向栈的栈顶。当向栈压入数据或从栈弹出数据时,esp的值不断变化,但无论如何变化,它都指向栈顶。

最后就是ebp,它用来把栈中的某个地址作为基址(基本地址,这样理解就是了),它用来标识栈中的某个固定位置,因此可以通过它访问这个固定位置附近的数据。

80X86的栈是向下增长的。也就是说,当向栈压入4个字节的数据时,esp = esp - 4; 当从栈中弹出4个字节时,esp = esp + 4。

以上!多么幸福的事情啊,32位汇编只需要在意这3个寄存器就可以了!(标志寄存器也挺重要的啊!但是跟本文要陈述的东西没太大关系,略)。



这你妹,解释下上图代表什么意思...纯粹照顾完全的新手。
首先,那一排格子代表内存空间中的一小段,每个格子代表4个字节。右边的十六位数值代表方格的地址。格子中间的“...”代表格子的内容。
图中地址是从下往上增长的。
esp永远指向栈顶。一开始它指向地址为0x0063 fff4的字节。然后向栈压入4个字节。
对80X86来说,指令就是push ...;
数据压入后,esp指向0x0063 fff0。这是新的栈顶。

弹出数据跟上面的过程相反。esp中的值会增加。

...这你妹,我画这干嘛,多此一举...好像画了图也没能说的更明白或者表达更深层次的意思啊。总之就是这样了,esp永远指向栈顶,记住就OK。

  • 5楼
  • 2011-02-18 03:55
关于80X86 32位CPU汇编模型就讲上面这些了。之所以讲这么少,因为这就是最基本的和最本质的内容,讲多了反而把重点搞没了。
总结就是记住3个寄存器。eip, esp, ebp。记住他们的意义就可以了。

2.栈帧与C函数调用
关于这个其实没有什么好讲的。

关于计算机,最重要的三个抽象是什么?答案是虚拟地址空间、进程、文件。

一个进程就是一个运行中的程序,或者被加载到内存中的程序。现代操作系统使进程看上去独占了所有的系统资源,但实际上系统中运行着多个进程。

所以从一个进程的视角看去,它独占了系统中的所有内存资源和CPU资源。对于32位系统虚拟地址空间被抽象为编号0~0xffff ffff的字节序列,它是平坦的,线性的,被系统抽象了的,所以叫它平坦地址或线性地址、虚拟地址。

对于Linux来说,保留高1G为系统使用。0-3G空间被应用程序也就是进程独占。

对于一个被加载了的程序也就是进程,其在内存中的分布为:

共享内存段
自由存储区(堆)
BSS段
数据段
只读数据段                         
代码段                             

栈向下增长。

每一个函数调用,都是一个栈帧。
以下代码:
int add(int x, int y)
{
    int z;
    z = x + y;
    return z;
}
int main(int argc, char* argv[])
{
    add(3, 5);
    return 0;
}
那么main函数是一个栈帧,add是一个栈帧。
当程序运行时,main函数栈帧先被建立,这个栈帧在高地址。然后调用add函数。此时add函数栈帧被建立,在低地址。当程序执行流进入add函数时,add函数内的局部变量在add函数栈帧中被建立。然后add返回。当add函数返回,此时add函数栈帧被销毁,同时add函数内的局部变量也被销毁。所以,C编程原则告诉我们:永远不要返回一个指向局部对象的指针。也就是说如下代码是错误的:
int* getNumber(void)
{
    int a = 3;
    return &a;
}

那么运行时的栈是什么样子的呢?它是一个随着运行,不断增长(进入新的函数调用)和缩短(函数返回)的动态影像。

OK,关于C栈帧就说到这里,完毕。

先把调用栈图发上来...
解释再说

  • 12楼
  • 2011-02-18 07:44
这你妹...有点看不清楚。点击看大图。要不将就看吧。

每个格子是一个字节。

左边是caller(调用者)栈,右边是callee(被调用者)栈(是同一个栈,分别是压参前、call指令执行后的状态。caller和callee的视图)。
图中画的内存地址是向上增长的。

首先,esp是栈顶,直接从caller栈顶看起。也就是,在调用前,esp指向某个内存地址。
在调用函数前将参数压入栈中。
push var1
push var2
这两行代码使esp - 8. 然后压参完毕,图中即为压参完毕esp.
然后调用函数:
call add
嗯,之前复习call指令时说什么了?call指令执行时,首先将返回地址压入栈。
也就是将add esp, 8 这条指令的地址压入栈。
如左图所示。

然后call指令执行过程调用,eip指向add函数内第一条指令的地址:
push ebp    ;将ebp保存到栈中,同时esp - 4(说过了80X86的栈是向低地址方向增长的).
此时ebp原值被保存入栈中。参看右图,蓝色部分是ebp原值。
然后:
mov ebp, esp
此时以ebp为基准的栈建立了。此时ebp和esp都指向栈顶(ebp原值被栈保存起来了哦)。
为什么要这么做?
因为esp是随时变动的,只要有压栈和出栈的操作,esp的值就随着压栈和出栈的操作变化(随着push和pop操作变化,甚或,程序员直接改动esp的值)。
而ebp却不会随着push和pop操作变化。程序员在callee中不会修改ebp的值,而是使用ebp作为基准访问参数。

那么接下来就很好理解了,第二个参数的地址是ebp + 8, 第一个参数的地址是ebp + 12.
所以
mov eax, [ebp + 8]      ;复制第二个参数值(var2)到eax
mov eax, [ebp + 12]     ;加上第一个参数值(var1)
就不难理解了。

在过程把实现代码处理完毕的最后,pop ebp将ebp原值从栈中弹出恢复。
然后ret返回指令将返回地址弹出并赋给eip(请注意,返回地址弹出后,esp + 4, 这时esp正好指向调用者压参完毕的位置),...
回到调用者的地方并继续执行。

那么调用处的add esp, 8               ;从栈移除参数
是干什么用的?注释已经说得很清楚了。
调用者将var1和var2压到栈中,由于调用者的压栈,esp被往下移动了8;那么这个esp的原始位置也就是caller的栈顶应该在过程调用后恢复,add esp, 8就是恢复esp的。

ok。基本上就是如此了!

对于C语言的过程调用,比如,在main函数里面调用add
int main(int argc, char* argv[])
{
    ...
    add(x, y);
    ...
}
实际上,这里add(x, y)(调用者处)被编译器编译成如下汇编代码:
push y
push x
call add
add esp, 8

以上,这就是C过程调用的汇编解释。

接下来给出一般过程的入口代码和出口代码。

  • 13楼
  • 2011-02-18 08:19
不难猜测,所有的过程(被调用函数)都有一样的入口代码和出口代码:

所有的C函数,在被编译器编译成汇编代码之后,
函数开始的几行汇编代码总是这样的,所以我们称这它为入口代码(entry code):
push ebp         ;保存基址
mov   ebp, esp    ;建立ebp偏移基准
sub esp, n      ;n个字节的局部变量参数
push ...         ;保存过程中会用到的通用寄存器
...
pushf             ;保存标识寄存器,也就是保存标志位

而结尾的几行总是这样的,所以称其为出口代码:
popf             ;恢复标识寄存器
pop ...         ;恢复寄存器
...
mov esp, ebp    ;恢复callee esp
pop ebp          ;恢复ebp
ret              ;返回

  • 14楼
  • 2011-02-18 08:31
4. stdcall和cdcel

既然已经了解了上述内容,那么调用惯例就很容易理解了。
cdcel和stdcall是约定俗成的调用惯例,它们的区别在于由谁来恢复esp。

cdcel是由调用者恢复esp的调用惯例,
也就是说
push var1
push var2
call add
add esp, 8
这是cdcel调用惯例

而stdcall则是由callee恢复esp的调用惯例
stdcall会在callee里面将ret这样写:
ret 8
意思是返回的同时esp + 8.

这两种调用惯例,stdcall的好处是不用每次都在调用过程后写add esp, 8这样就减小了代码量,减小了目标文件的体积。
而stdcall的缺陷更明显,那就是callee有时候无法推断参数的个数和长度,这样的话esp只能由调用者恢复(比如变参数函数,这种函数callee是无法推断参数个数的,也就无法知道应该在ret后面加多少偏移量)



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值