汇编之路-复习栈操作

不得不承认上次关于栈桢和栈操作写得有些笼统,这里做一次“补充”,美名其曰:“复习”。

下面的这个例子几乎就能覆盖所有的栈操作相关的内容了。
void dummy()
{
        int     i = 12;
        int     j = 13;
        char    c = 'a';
}

int main()
{
        dummy();
        return 0;
}

下面是利用MDB(注[1])反汇编的代码:
> main::dis
main:                           pushl   %ebp
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+0×10:                      call    -0x2a          
main+0×15:                      movl    $0,%eax
main+0x1a:                      leave
main+0x1b:                      ret

> dummy::dis
dummy:                          pushl   %ebp
dummy+1:                        movl    %esp,%ebp
dummy+3:                        subl    $0xc,%esp
dummy+6:                        movl    $0xc,-4(%ebp)
dummy+0xd:                      movl    $0xd,-8(%ebp)
dummy+0×14:                     movb    $0×61,-9(%ebp)
dummy+0×18:                     leave
dummy+0×19:                     ret

分析上面的汇编代码我们要解决如下几个方面问题:
1、过程调用的标准模式
我们知道发生过程调用的指令是call,那么call做了些什么呢?上面每个过程的最后都有leave指令,它又作了什么呢?我们不妨来跟踪一个栈帧的形成过程,分析后自然会有答案。

(1) 我们从main + 0×10处开始,这里是一个call指令,此时的活动栈帧为main的栈帧,dummy栈帧尚未形成:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————
|    A     | main栈帧栈底 <– %ebp
+———-+
|    B     |
+———-+
|    C     | main栈帧栈顶 <– %esp
+———-+
|          |
+          + 0×00000000

(2) 调用call指令后,未执行dummy前,此时main的栈帧已经结束,%eip中存放dummy起始指令地址准备执行。
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     |
+———-+
|          | dummy的返回地址, main栈帧栈顶 <– %esp
+———-+ —————————
|          |
+          + 0×00000000
可见call首先将main调用的函数(这里是dummy)的返回地址pushl到栈中,形成main栈帧的最后一个部分,然后跳到dummy的起始处。所以call等价于下面两条指令:
pushl %eip  //将下一条指令地址压入栈中
jmp dummy

(3) 形成dummy栈帧
dummy首先将main的栈底保存起来,然后创建自己的栈底。
+          + 0xffffffff
|          |
+———-+
|          | dummy的返回地址,属于main的栈帧范畴
+———-+ —————————
|    D     | dummy栈帧栈底 <– %ebp,存储着main栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶 <– %esp
+———-+ —————————
|          |
+          + 0×00000000

(4) dummy返回
dummy返回时调用的第一条指令leave,该指令相当于如下两条指令:
指令1: movl %ebp %esp // 将%esp置到dummy栈桢首部

该指令执行后状态如下:
+          + 0xffffffff
|          |
+———-+
|          | dummy的返回地址,属于main的栈帧范畴
+———-+ —————————
|    D     | dummy栈帧栈底 <– %esp <– %ebp
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

指令2:popl %ebp
该指令执行后状态如下:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————-
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     |
+———-+
|          | dummy的返回地址,main栈帧栈顶 <– %esp
+———-+ —————————
|    D     | dummy栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

dummy返回时调用的第二条指令ret,该指令相当于popl %eip,执行完内存栈的情况如下:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————-
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     | <– %esp main栈帧栈顶
+———-+
|          | dummy的返回地址
+———-+ —————————
|    D     | dummy栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

至此,main的栈桢又再次被恢复了。

经过上面分析,得出过程调用标准模式如下:
pushl %ebp
movl %esp %ebp

//过程体

leave
ret
其中ret和call对应,而leave则和最开始的那两句对应。

2、访问局部变量
在dummy的汇编码中我们可以清晰的看到对三个局部变量i,j,c的赋值语句:
movl    $0xc,-4(%ebp)
movl    $0xd,-8(%ebp)
movb    $0×61,-9(%ebp)
其三者有一个共同点就是“都是通过对%ebp的偏移来访问局部变量的”。

3、局部变量的分配
两个以上的局部变量的栈上分配涉及到栈内存的对齐问题,dummy的代码足以说明问题。我们在dummy的栈桢中分配了两个整型和一个char型变量,实际需要9个字节。那我们来看看汇编是否给我们只分配了9个字节呢?
movl    %esp,%ebp
subl    $0xc,%esp
movl    $0xc,-4(%ebp)

可以看出subl $0xc,%esp一句在内存栈上为我们留出12个字节的空间,在char c的后面又多分了3个字节,以保证对后面的变量的地址访问是对齐的。

4、对异构类型变量的分配和访问
举例如下:
struct test_t {
        int i;
        int j;
        int a[3];
};

void dummy()
{
        struct test_t t;
        t.i = 11;
        t.j = 12;
        t.a[0] = 'a';
        t.a[1] = 'b';
        t.a[2] = 'c';
}

int main()
{
        dummy();
        return 0;
}

> dummy::dis
dummy:                          pushl   %ebp
dummy+1:                        movl    %esp,%ebp
dummy+3:                        subl    $0×28,%esp
dummy+6:                        movl    $0xb,-0×28(%ebp)
dummy+0xd:                      movl    $0xc,-0×24(%ebp)
dummy+0×14:                     movl    $0×61,-0×20(%ebp)
dummy+0x1b:                     movl    $0×62,-0x1c(%ebp)
dummy+0×22:                     movl    $0×63,-0×18(%ebp)
dummy+0×29:                     leave
dummy+0x2a:                     ret

与上面的例子不同的是这次为了存储一个test_t类型结构,栈居然留出了0×28(40d)大小的空间,在t.a[2]与%ebp之间留了0×14(20)个字节空闲。这里的原因不得而知。如果是为了对齐,那么这个代价着实不小。

[注1]
在X86平台的Solaris9上,GDB反汇编使用的语法与我们的稍有差异,而使用Solaris自带的MDB(The Modular Debugger)则和我们的汇编语法保持一致。顺便说一句MDB是一个强大的调试工具,在Sun公司的网站上有其详细的使用说明。

软件抽象

不知道“软件抽象”这个标题能否恰好表达出我想表达出的意思,暂且就起这个名字吧。随着工作经验的增加,对软件开发所涉及的技术知识体系的理解也渐渐地清晰(起码自己是这么感觉的^_^),思考了若干时间后,拿出来给自己一个和大家交流的机会。

1、起源
这个想法起源于一次项目方案讨论例会,会上我们的项目遇到了“存储资源瓶颈”,遂有同事提出一个类似“数据中心”的方案,但考虑到部门目前没有相关经验和数据供参考,大家就否定了这一想法,会上我也并不赞同这一方案。会后坐在电脑前仔细考量了一下,又想起以前部门一弟兄曾提出的“将业务和通讯协议分离”的想法,感觉这是一个很有“前途”的方案。考虑到部门目前的业务模式和软件类别,将“协议和存储”从各个系统中分离出来将是一个很“革命性”的做法。会后又和两位要好的“战友”探讨过这个问题,他们也一致赞同。虽然我们的想法是美好的,但是毕竟我们不是领导,我们也只能在私下“愤青”一把。

2、观点
“软件是存储、通信、UI(user interface)和业务逻辑的紧密结合体。”
a) 在软件的生命周期中,较稳定的是存储和通信,最易变化的是业务逻辑;
b) 在软件的层次上,存储和通信一般处于底层,而业务逻辑处于最上层;
c) 不同类别的软件,其侧重点有所不同。如对于应用程序,其关注点应该在“业务逻辑”。

这些观点也许并不是什么新颖的,你可能在很多资料中都曾见到过类似的字眼儿。我觉得上面的观点有这么三点作用(现在我想到了三点^_^)。

a) 指导你的学习,制定你自己的学习计划。

我自己也回顾了一下我入司后的学习历程,其实也都是围绕着这个观点。下面简单列举几个每个方面涉及的知识领域:
存储 — 虚拟存储系统(自认为是存储技术的一次标志性技术,在《深入理解计算机系统》一书中有很好的阐述)、文件IO等。
通信 — 内部通信包括进程IPC、线程管理等,这种通信技术较为成熟;外部通信包括TCP/IP、Socket等。
业务逻辑 — 现在很多建模技术及软件过程都是围绕和针对业务逻辑的,如UML、RUP和敏捷过程等。
UI — 这个并不是很熟悉,但我相信在这方面也有太多的知识和技巧了。

b) 理解软件的发展
软件技术发展依旧那么迅速,我们可以从上述四个方面来理解:
存储 — 如微软即将在下一代操作系统中推出的新一代的文件系统、新一代数据库技术等;
通讯 — 内部通讯技术经过几十年的发展已经趋于成熟,但是外部通信技术还在突飞猛进的发展,如分布式、IPV6、VOIP、及时通讯技术以及在更火爆的无线通讯领域的各种技术等等;
业务逻辑 — 如Ivar最近提出的“主动软件”及SMART过程等;
UI — 我关注的不多,不举例了。

c) 思考你的设计
就像我们的项目,更加清晰的设计就是将存储、通信方面都都分离出来作为底层的支撑系统。对应这四个方面思考你的设计,不知道你是否已经有了些许想法呢。

3、小结
写到这也许起名为“软件抽象”有些言过其实。但一时也想不出什么更加“地道贴切”的名字,就暂用它撑撑门面吧!^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats