深入研究C语言 第二篇(续)

1. 关于如下的程序,关于结构体的拷贝,拷贝是拷贝到内存中的什么地方?

我们进入debug进行反汇编,单步等操作跟踪查看。发现:

在main中,我们看到call 0266应该对应的是转跳到func处执行。

在这里,func赋值完成后,又call到了0B3D:13EA处,这里应该是其向内存中复制的函数。我们查看。

首先我们看LDS:从存储器取出32位地址的指令.和LES:LES( load ES)指令的功能是:把内存中指定位置的双字操作数的低位字装入指令中指定的寄存器、高位字装入ES寄存器。

我们执行中查看:

我们看到,这里DI和SI被放入两个数,大概是要将两边的数进行复制。而通过上下文的程序我们可以猜想是将DI处也就是FFD0处的数据复制到0428处。我们先看FFD0处的数据:

在看执行完后的数据:

我们看到数据确实被放到了0428处,并且,后续的程序中还有从0428处向栈中赋值的程序。所以我们确定,结构体就是被拷贝到了0428处。

那么,程序这样做的原因是什么呢?我们知道,在普通变量返回的时候,程序是用将变量值放在ax中进行返回的(long型等四字节变量是用dx,ax返回,这里不再贴图)。但是结构体变量,他的长度是不确定的,程序并不知道其长度到底有多长,并且,在大多数情况下,结构体的长度要大于四字节,普通的返回不能满足这种要求。所以程序就选择了ax返回的结构体变量首地址。那么,为什么要有复制的过程呢?我们知道,在函数内的变量是局部变量,是在栈中的。在函数调用完成后就被释放了,如果我们传出的是栈中的地址,那么这个数据就消失了。所以,才有了从栈中复制到数据段中,在将数据段中的地址传出到上一层函数这样的机制。

2. 关于汇编程序中的跳转语句,为什么要使用跳转,关于返回值的研究(1),返回值一个,直接跳转到下一条指令?(2),返回值多个时是如何处理的?

我们编写程序如下:

我们看到其在debug中实现的方式如下:

我们看到,在f函数返回的时候,在函数return后(将返回值放入AX中后),下一句是jmp 020f。而020f处是ret语句,这是什么意思呢?我们很容易想到,在函数中,执行过return语句后,函数就结束返回了。我们编写下面这个函数:

然后我们debug进入查看:

我们看到,在两个return后都有jmp语句,且都是jmp到ret这条语句。也就是说,无论什么情况下,只要是执行了return语句,函数都会退出。而退出是用ret语句退出的。我感觉,这样的好处是,首先,函数有一个单一的开始地址,而且有一个单一的结束地址,其次,这样能减少一部分语句的量,避免函数中有多个ret语句。

那么,如果返回的是多个值呢?我们编辑查看。

我们看到,这里返回的时候依然是使用的jmp语句。

由此我们可以得出结论,jmp语句在执行后函数就返回了,靠的是jmp到ret指令。

3. 分别对全局变量、局部变量、参数在何时申请内存、在何时释放内存这两点进行对比性的总结。

 

全局变量


局部变量


参数


申请内存


在主函数调用之前程序的语句中被分配


在函数调用的时候在栈中被分配


在主函数调用带有参数的函数前的准备语句中被创建


释放内存


在主函数调用完成后,程序在释放资源的过程中被释放。


当函数结束调用的时候,局部变量在栈中的空间被释放。


当函数调用结束后,参数的空间被释放。

4. 修饰符static、auto、const、register的作用?

首先我们来看着四个修饰符的说明:

extern 外部变量

static 静态变量

auto 自动存储变量

register 寄存器变量

然后,我们编写一个程序如下,查看他们各自的作用。

我们发现:

说a不能再main内定义。我们改变程序:

编译连接后我们查看:

我们结合其各自的名字发现:register 寄存器变量其值放在了寄存器中,而auto 自动存储变量自动转换成了局部变量。

我们在这样更直观的看一下:

我们看到:

extern 外部变量 放在数据段中 与全局变量类似

static 静态变量 放在数据段中 与全局变量类似

auto 自动存储变量 放在栈段中 与局部变量类似

register 寄存器变量 放在寄存器中 是一个寄存器变量

5. 静态局部变量一直占用内存空间,C语言这样设计有何用意?

关于静态局部变量,我们编写这样一个程序:

我们查看其执行结果:

我们看到,f1中的static变量,在两次调用的时候值没有被消除,而是持续的向上叠加(这是由于a++)。而且,这个变量与其上级函数f中的变量a值并不相同,这里可以认定其并非一个变量。我们在看在两个函数中定义static变量时的情况:

我们查看其运行结果:

在这里我们看到,f1中的static变量与上级函数f中的static变量值依然不相同,我们看起反汇编的结果:

在这里我们可以明显的看到这两个static变量是相邻的两个变量。而f1函数在调用static变量a的时候,一直是在调用042c处的变量。其并不想局部变量,在函数结束时被释放,而是一直存在在内存中。

这样做的好处是什么呢?这样做可以让C语言中的变量类型更加丰富和自由,我们想定义某些计数的变量时,直接使用static变量会更加的方便。也就是说C语言在设计时,就考虑到了不同程序中对不同变量的使用要求,在C语言中加以实现。

6. 变量赋值的顺序与存储顺序一样?定义不同类型变量是什么情况?(赋初始值,不赋初始值,其中一部分赋值)

我们首先来查看一下定义相同类型的时候,赋值的顺序与存储顺序的关系:

我们可以看到,在这种情况下变量赋值的顺序与存储顺序一样,但是这里无法分清楚到底是赋值的顺序与存储顺序一样,还是定义的顺序与存储的顺序一样。

我们接着查看,不同类型的变量,赋值的顺序与存储顺序的关系:

这里发现,在这种情况下变量赋值的顺序依然与存储顺序一样,但是这里还是无法分清楚到底是赋值的顺序与存储顺序一样,还是定义的顺序与存储的顺序一样。

然后,我们看一下,到底是赋值的顺序与存储顺序一样,还是定义的顺序与存储的顺序一样。

在这里我们看到:

这样我们就可以说明了,变量定义的顺序与存储的顺序一样。并且他们的顺序是相邻的。

我们编写这样一个函数:

在仔细研究其反汇编后的指令后,我们发现其分配的空间出现了偏差。我们分析后发现其中的int b,long d,long e并没有分配空间。这是为什么呢?我们看到这里这些变量定义后并没有使用。那是不是因为没有使用,所以程序就不给分配了呢?我们将的做一个赋值:

我们发现,d有赋值语句后,程序就给d分配了一个在c与f之间的4字节的内存空间。

那么程序这样做有什么好处呢?在整个程序都没有用到的变量,就不给分配内存,这样不会使得程序有逻辑或者运行时的错误,因为程序没有用到那个变量。还可以减少没有用的内存的分配,使得程序更加有效率和更节省空间。这是C语言为变量分配是遵循的一种机制。

7. 函数声明的作用是什么?函数声明是否有对应的代码?

首先我们编写一个程序:

我们查看其反汇编的代码:

发现函数声明并没有对应着函数代码(如果有的话,应该再01fa处,因为从前面的程序我们知道,01fa前的语句长度是固定的,如果在前面有函数声明的代码的话,mian函数对应的01fa地址会相应的后移,这里没有后移,01fa处也没有语句。函数声明并没有对应着函数代码。)

那么函数声明的作用是什么呢?在这里,我觉得(这里不再展示有return返回值,有参数调用的函数的反汇编代码)函数声明是为了在编译的时候,对不同的函数采取不同的实现方式。比如,对于有参数的函数,在开始前就应该传入参数,在结束后应该释放参数。有返回值的函数,在调用前就不应该让ax中有没有保护的值等等。

8. 研究结构体参数传递的三种形式:(1)结构体变量作为整体传递;(2)结构体变量某几个成员变量作为参数传递;(3)结构体的指针传递。

对于结构体变量作为整体传递,我们已经研究过,其原理是通过将数据段中的数据复制到栈段中,这样来传递结构体变量,下面我们研究结构体变量某几个成员变量做参数传递。

我们看到,这样的参数与平常的参数相同,都是调用前入栈,然后函数内使用时就在栈中使用。

我们在看看指针类型的传递:

我们看到,指针类型的传递是将结构体变量的首地址传入到函数中。

时间: 2024-10-12 19:55:45

深入研究C语言 第二篇(续)的相关文章

深入研究C语言 第一篇(续)

没有读过第一篇的读者,可以点击这里,阅读深入研究C语言的第一篇. 问题一:如何打印变量的地址? 我们用取地址符&,可以取到变量的偏移地址,用DS可以取到变量的段地址. 1.全局变量: 我们看到,这里的全局变量是在数据段中的. 2.局部变量: 我们看到,这里的局部变量是在栈段中的. 问题二:研究main函数的偏移地址与源代码中main函数的定义位置之间的关系. 我们打印函数的偏移地址,在打印的过程中我们可以发现: 当程序编码如下时,程序运行的结果是: 而将程序的f1函数和f3函数互换,程序运行的结

深入研究C语言 第二篇

1. 程序一: 首先我们研究如下程序: 回答如下问题: 1. 程序运行时n,a,b,c的段地址在哪个寄存器中? 全局变量的存储空间在什么段里?局部变量的存储空间在什么段了?参数在什么段里?函数的返回值存储在什么地方? 全局变量的存储空间在什么时候分配?什么时候释放? 局部变量的存储空间在什么时候分配?什么时候释放? 2. 函数f3在调用与返回方式与函数f1与f2有何不同? 我们编译完成后,进入debug查看. 首先,我们执行到main函数处,然后开始单步执行.我们看到,每次单步执行时,涉及到取数

深入研究C语言 第一篇

一. 研究过程 1.第一章:创建编译环境: 我们首先下载TC2.0,找到其中与编译连接相关的程序和文件: (1) 编译器:TCC.exe (2) 连接器:tllike.exe (3) 相关文件:c0s.obj.cs.lib.emu.lib.maths.lib 将文件放在C:\C目录下. 编写程序测试我们的编译环境: 在这里我们看到,程序被正常的编译.生成了.exe文件.并且可以正确执行. 当然,在TC中,c0s.obj.cs.lib.emu.lib.maths.lib这四个文件时在TC目录下的l

C语言中容易被忽略的细节(第二篇)

前言:本文的目的是记录C语言中那些容易被忽略的细节.我打算每天抽出一点时间看书整理,坚持下去,今天是第一篇,也许下个月的今天是第二篇,明年的今天又是第几篇呢?--我坚信,好记性不如烂笔头. 第一篇链接:C语言中容易被忽略的细节(第一篇) 1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来.C语言中数组元素可以是任何对象,也可以是另外一个数组,即数组的数组. 2.C语言允许初始化列表出现多余的逗号.例如:int days[] = {1,2,3,};作用:方便自动化生成代码.

「C语言回顾之旅」第二篇:指针详解进阶

说明: 第一篇回顾了指针的基本概念以及基本使用,因此对指针也有了一个较为清晰的思路,但实际上第一篇关于指针的内容是不太容易忘记的.这是第二篇中的内容是比较容易混淆,但对于指针的进一步学习也是非常重要的. 一.指向函数的指针 1.函数指针 ·函数指针即指向函数的指针,函数指针值为函数的入口地址,通过使用该指针,即可以使用该函数: ·编写一个程序返回两个数的最大值,通过函数指针调用函数: a.main函数代码如下: #include<stdio.h> int max(int *, int *);

第二篇 SQL Server代理作业步骤和子系统

本篇文章是SQL Server代理系列的第二篇,详细内容请参考原文. SQL Server代理作业由一系列的一个或多个作业步骤组成.一个作业步骤分配给一个特定的作业子系统(确定作业步骤去完成的工作).每个作业步骤运行于一个单独的安全上下文,尽管每个作业有一个所有者来决定谁可以修改作业.本篇主要关注组成SQL Server代理的作业步骤和子系统.快速回顾作业理解SQL Server代理作业的最佳方式是把相关联的 需要完成给定任务 的组件放在一个容器中.作业最主要的组件有作业步骤.计划.警告和通知.

认识IL代码---从开始到现在 &lt;第二篇&gt;

·IL代码分析方法 ·IL命令解析 ·.NET学习方法论 1.引言 自从『你必须知道.NET』系列开篇以来,受到大家很多的关注和支持,给予了anytao巨大的鼓励和动力.俱往昔,我发现很多的园友都把目光和焦点注意在如何理解IL代码这个问题上.对我来说,这真是个莫大的好消息,因为很明显我们的思路慢慢的从应用向底层发生着转变,技巧性的东西是一个方面的积累,底层的探索在我认为也是必不可少的修炼.如果我们选择了来关注这项修炼,那么我们就应该选择如何来着手这项修炼,首先关注anytao的『你必须知道的.N

java学习笔记 第二篇 核心技术(二)

第十四章 集合类 集合类用来存放对象的引用.继承关系如下图: 14.1 Collection 接口 是层次结构中的根接口,构成Collection的单位称为元素.Collection接口不能直接使用,但该接口提供了添加元素.删除元素.管理数据的方法. Collection接口常用方法: 14.2 List 集合 包括List接口以及List集合的所有实现类.List集合中的元素允许重复,各元素循序就是对象插入的顺序 1.List接口,两个重要方法: get(int index): 获取指定索引位

读书笔记:《如何阅读一本书》(暂定第一篇、第二篇)

我阅读这本书的最初动力: 我希望能够通过这本书得到正确的阅读方法,让我在阅读书籍的时候,能够真正的吸收进去,而不是表面的看完.系统的进行学习方法论并且能够亲身实践的方法论. 背景: 作者认为当下的人们,只通过了最基础的阅读训练,但是实际上对于一些严谨或者说需要严密思考的章节的阅读能力是不足的.所以出了这本书,让人们了解如何阅读一本书,分了四个章节:首先讲解了阅读的艺术,以及阅读的层级,基础阅读以及检视阅读的层次如何做到,还有什么样的阅读者才能够真正的阅读进去.接着讲解了最重要的一种阅读方式,分析