c语言与linux变量,从汇编来看C语言之变量-程序员宅基地

技术标签: c语言与linux变量  

1、基础研究

对如图程序进行编译连接,再用debug加载。

ba69f85bae556589215190c565e1c57e.png

我们在偏移地址1fa处查看main函数的内容:

4f3b83a195ea361439d5927e33e6098a.png

执行到1fd处,发现n的偏移地址为01a6,段地址存储在ds寄存器里,为07c4.

481aa2f50b6e3fec37d15e881b5e9f14.png

再查看函数f2:

308028aa782c68bc7e18dd6a38af86b5.png

参数a、b的值是用栈来传递的,它们的段地址都存放在ss寄存器中:

ae71ea6377c8661edd685a906a06c0d7.png

局部变量c的值在这里是用si寄存器存储的,因为c正好是int型,那么子函数里定义的局部变量是用寄存器存储吗?我们在这里加一条赋值语句看看会如何:

a1c6402262f431eb3c2468bdd39fe46b.png

9a0eaff7222fee859b00337ea815525a.png

可见,局部变量d是放在栈里的,而c是放在寄存器si里的,只是函数要将c返回,就将c的值赋给了ax。那么如果返回值不是int型怎么办?这个问题我们之前已经研究过:如果是1字节的数据,用al存放,如果是4字节的数据,高16位用dx传递,低16位用ax传递。

也就是说全局变量n的段地址在ds寄存器里,局部变量a、b、d的段地址在ss寄存器里,局部变量c的值存储在寄存器si里而不是内存里,没有段地址。所以全局变量n存储在程序开始的数据段里,而局部变量c存储在栈段里。参数a、b存储在栈段里。函数的返回值按值的大小存储在寄存器ax和dx中。全局变量的存储空间在程序开始就分配了,在整个程序执行完才释放,分配和释放的工作应该是由c0s.obj里的函数完成的。局部变量的存储空间在什么时候分配呢?我们将增加局部变量d的函数f2与之前的函数f2对比,发现多了一条语句“sub sp,2”,之后对d的赋值语句为“mov word ptr [bp-2],4”,这说明“sub sp,2”就是为局部变量d分配栈段空间的指令,局部变量是在子函数开始执行时分配的,那么是在函数入口处将局部变量全部分配,还是在函数中局部变量定义处分配呢?因为TC2.0所使用的c标准要求在函数开头将要使用的变量全部定义,所以在这里这两种方式是一样的。而函数结束时“mov sp,bp”指令将sp的值还原,也就是释放局部变量d的空间,所以局部变量的存储空间是在函数结束时释放的。从程序中可以看到,函数参数的存储空间是在主函数里对函数进行调用时就分配的,也就是将参数的值入栈,而在函数返回后,用pop cx将参数从栈段中释放。

主函数里调用f3函数使用的语句是“call 076a:0239”,也就是直接call函数的段地址+偏移地址,我们来看f3函数的内容:

ec5c0a5ac88b9aedea11717a0d327eb5.png

发现f3返回时是用retf返回的,也就是将ip和cs都出栈。所以对于far型的函数,调用时要用call 段地址+偏移地址,返回时要用retf将段地址和偏移地址都出栈。

再来看程序2:

76ceda7bb15f258ffbb18f09cbf5e9ec.png

观察函数f的内容:

976c0c3821e73fe81eea51d0c068b4c0.png

发现n的存储空间为si寄存器,a的存储空间为以ds:0194为地址的两个字节。它们的存储空间是什么时候分配的呢?我们知道局部变量n的存储空间是在函数开始时分配的,而a的存储空间是固定的内存空间,不是栈段,在函数结尾处n的空间被释放了而a的空间并没有被释放。在网上查阅资料得知,静态局部变量和全局变量分配存储空间的方式是相同的,而且具有相同的生命周期,只是静态局部变量只能在定义的函数中使用。

d8980cd90b224503a8f7eba5fe12a869.png

观察主函数,也没有释放静态局部变量的语句,可见静态局部变量的存储空间也是由c0s.obj里的函数进行分配和释放的。

我们观察程序的执行结果也可以发现:

04b6da9db1baec51a6113189ce7da8bb.png

不管执行多少次f函数,每次输出n的值都为1,因为它是局部变量,f函数结束后就要释放,而a是静态局部变量,相当于全局变量,它的值是可以不断累加的。

再来看程序3:

1d9def31480070d88c016994d1bc2a7e.png

main函数的内容为:

5fe4eaa2a4c6e27d483c1cb9b13cd943.png

这里的a、b、c、a1、a2都是全局变量,只是它们的类型不同而已。他们的存储空间是否相邻呢?看看偏移地址194处的数据段的内容:

983ac7435bfce217707acc89b3f6632f.png

可以看到数据段里存储了5个值为1的数,它们的存储空间是紧邻的。

整型的存储空间为2个字节,字符型为1个字节,长整型为4个字节。

在自加1运算时,整型是inc word ptr,对1个字的数据进行操作;字符型是inc byte ptr,对1个字节的数据进行操作;长整型是先对低四位数据进行运算,再用位运算符adc对高四位进行运算得到结果。

下面来看程序4:

c6c62140404881b9e14bdcf8060fbeff.png

观察main函数的内容:

7ce7fbd9fa8d64b31daac82ef5d4de96.png

d2b252b3a118b733182a305c693d2f8a.png

71da91dc2874da71a87e8798b4a5a4be.png

a687ea21b03ab2e75dadd7e253c299d1.png

我们注意对变量a、b各个数据项的赋值部分:a的每个数据项都有固定的内存地址,而b的数据项都是存储在栈段里面,因为a是全局变量而b是局部变量。而且a、b里面的数据项的各自的存储空间是相邻的。

观察发现,在赋值语句后,程序还有一大段的指令,这些指令是用来执行printf函数的功能的。

下面来看程序5:

2648b214dd6ef9252844ca361fa00e81.png

main函数的内容有:

99a28cc320dda33c2d99294eac5cdca7.png

7d6ba9f37e221cada371ed6307901aeb.png

7f83885f271ef0123f285c06611dc52a.png

观察发现程序中出现了lea指令,查询可知lea指令的作用是取偏移地址。程序里面出现了很多call指令,经过实验,发现调用f函数的是call 0256,调用func函数的是call 0266.main函数是怎么把结构体数据a传给函数f的呢?我们先看看f中调用的结构体数据在什么地方:

f5f8ea680fe0a5b9fdd28b34323cceca.png

调用的数据在栈段里面,a.a是bp+4,a.b是bp+6,a.c是bp+8.

那么main函数传值应该是将数据项压栈的过程。

但我们发现在main函数里从语句call 0266到call 0256之间没有压栈的语句,只是调用了两个函数:call 076a:1085和call 076a:10a1,这两个函数肯定是对结构体数据和栈进行处理的,但是我发现难以看懂看懂它们的内容。那么不如换一种思路,我们先看看func()返回的内容放在什么地方,下面是函数func的内容:

53fc32369ae69f133c95659dc4dd43f6.png

bc770adb9c7c860247be6c44416ce316.png

我们发现func在对数据项进行赋值后,同样调用了076a:1085处的函数,而与main函数中比较,main函数是将ds、ax寄存器压栈,而这里是将ss、bx寄存器压栈,即将数据项的段地址和第一项的偏移地址压栈,再调用076a:1085进行处理。但是这个函数具体有什么作用呢?我还无法得出结论。在网上找到下面一段话:

C 语言中函数返回结构体时如果结构体较大,则在调用函数中产生该结构的临时变量,并将该变量首地址传递给被调用函数,被调用函数返回时根据该地址修改此临时变量的内容,之后在调用函数中再将该变量复制给用户定义的变量,这也正是C语言中所谓值传递的工作方式。

如果结构体较小,则函数返回时所用的临时变量可保存在寄存器中,返回后将寄存器的值复制给用户定义的变量即可。

我对这段话的理解是,函数076a:1085创建了一个临时变量,将局部变量的结构体对象a的各项数据复制到这个临时变量里,之后函数func结束,func里的变量a从栈中被释放,之后main函数再调用076a:10a1,将这个临时变量的值压栈传给函数f使用。

再来看看076a:10a1的内容:

560303f1e670f2d970449c5a8810b050.png

a691c3a8b1d7b15a32ff31b4fdf574c4.png

观察函数内容,发现这就是一个搬移函数,将数据项从原来的位置搬运到栈段中指定的位置,以供函数调用。076a:1085的功能也是类似的。所以从函数传递结构体型的数据是调用搬运函数,用movsw或movsb指令将数据项搬运到栈段中供函数调用。因为时间关系,这里不能仔细研究,之后再继续完善。

2、拓展研究

问题:

(1)

660a71757493c498d147c1e947727e30.png程序1函数f2里076a:0234处的语句jmp 0236指向的是下一条语句,这不是无意义的吗?它起什么作用呢?

答:这里jmp语句是跳转到释放局部变量的结束语句,所以我的猜想如下:1、编译器为了避免程序出错所以要用jmp精确跳转到结束语句。2、编译器给程序预留了一个接口用来存放其他功能的程序。

这里函数返回语句是在函数最后面,如果是选择语句或者有多个返回语句的情况,就会出现这种情况。

(2)函数里局部变量都是第一个定义的在si寄存器里,其他的在栈段里吗?

答:不是,经过实验,只有当该局部变量需要返回时,才存储在si寄存器里,否则只是存储在栈段里。

(3)静态局部变量与全局变量的区别就在于在后者整个程序的所有函数里都能访问,而前者只能在定义的函数里访问吗?

答:最明显的区别就是作用域的区别。

(4)加载第3章的5个程序。查看偏移地址为1fa处的指令,为什么有的程序有“push bp”和“mov bp,sp”两条指令,有的程序没有?

答:我的5个程序都有保护语句,如果没有可能是编译器的问题。

如果用TC2.0编译,是有的,如果用tcc编译,会出现这种情况。

(5)程序1中,全局变量n,是由“unsigned int n”这条语句定义,还是由main函数中的“n=0”这条语句定义?

答:应该是由前者定义的,函数外定义的变量,不管有没有加static,没有初始化的话,系统默认初始化为0。如果在n=0语句之前打印n,是能够打印出它的值的。

(6)结构体中数据项的存储为何不使用push、pop 指令进行操作?

答:题目的意思应该是结构体型数据参数的传递和返回是怎么实现的,我们已知是通过搬运函数来实现的,即将存储结构体函数的数据段的值整体移动到一个栈段中。那么为什么不通过push、pop实现呢?我觉得理由如下:1、c语言是将结构体作为一个数据类型的,和int、char等数据类型一样,所以对它的处理方式和其他数据类型是一样的,即要对它整体来处理,如果用push、pop的话,就要对它里面的数据项分开来处理,这是不符合我们建立结构体数据类型的初衷的。2、如果要对它里面的数据项分开来处理,就要知道它里面有哪些数据项、有几个,那么就需要进行统计,这个是不好实现的(我还没找到实现的方法)。3、我们只需要实现传值的目标而不需要在这个过程中对数据进行处理,那么就要选择最简单快捷、开销最小的方法,很显然块搬运是最好的方式。

(7)程序4中,在声明的局部变量struct stu b的后面,假如在后面定义一个char型变量,所占用的字节数为6(char型数据所占用字节数+局部变量struct stu b的数据项所占字节数);假如在后面定义一个整型变量,所占用字节数8,此时有了1个字节的填充,为什么?

答:

dada4c95d13e2a3c62201d9208c35a2a.png

e是int型,eee是char型,前5个变量所占空间为7个字节,加上eee才8个字节。

a8698c68dc7e20769b44e0b2b122465e.png

21c6c9bb968946f4d9d20cbd5cc01c9d.png

局部变量的情况是一样的,加上int型的e和char型的eee也才8个字节。

如果在结构体数据后面再添加一个独立的int型数据,会出现这种情况。内存对齐的结果,结构体内外都有可能出现。

(8)重新研究,不同类型的变量,存储空间的分配情况。

答:char型变量占1个字节,int型的变量占2个字节,long型占4个字节。int型在TC里占2个字节,在VC字节里占4个字节。因为TC模拟的是16位dos操作系统,VC模拟的是32位操作系统。

(9)再次对程序5进行研究,找到每一条c语句对应的汇编代码。

Struct n a;

Int b;--------------------sub sp,6

a=func();----------push ss;

push bx;

call 0266;

push ds;

Push ax;

mov cx,6;

Call 076a:1085;

b=f(a);------------lea bx,[bp-6]

Mov dx,ss

Mov ax,bx

Mov cx,6

Call 076a:10a1

Call 256

Printf(“%d”,b);---mov si,ax

Push si

mov ax,194;

Push ax

Call 093a

Printf(“%d”,f(func()));---call 266

Mov dx,ds

Mov cx,6

Call 076a:10a1

Call 256

Add sp,6

Push ax

Mov ax,198

Push ax

Call 93a

Func():

Struct n a;-----------sub sp,6

a.a=1;----------------mov word ptr [bp-6],1

a.b=2;----------------mov word ptr [bp-4],2

a.c=2;----------------mov word ptr [bp-2],3

Return a;-------------mov bx,426

Push ds;

Push bx;

Lea bx,[bp-6]

Push ss;

Push bx;

Mov cx,6

Call 076a:1085

(10)全局变量、局部变量存储方式的不同有什么普遍的意义?

答:我们把这里的局部变量理解为动态局部变量。全局变量的存储空间是固定的,局部变量是动态分配的,他们的存储方式决定了他们的特点:1、作用域。全局变量在该程序所有地方都可以使用,局部变量只能在定义的函数里使用。2、生命周期。全局变量的生命周期和整个程序是一样的,而局部变量的生命周期与函数一样,函数结束即释放。这种方式更有利于减小程序的内存开销,避免变量定义出错,保证函数的独立性,使程序模块化,方便编写和调试。

全局变量放在数据段中,局部变量放在栈段中。比如一个程序有100个函数,每个有5个局部变量,如果都放在数据段中,就会造成内存开销太大,不好管理和调用,所以要用栈段来存放局部变量,这就是高级语言的核心机制。

3、研究总结

本章研究了函数的各种类型的变量的存储方式,是比较重要的一章。

0b1331709591d260c1c78e86d0c51c18.png

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_36065817/article/details/116865933

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签