深入研究C语言 第二篇

1. 程序一:

首先我们研究如下程序:

回答如下问题:

1. 程序运行时n,a,b,c的段地址在哪个寄存器中?

全局变量的存储空间在什么段里?局部变量的存储空间在什么段了?参数在什么段里?函数的返回值存储在什么地方?

全局变量的存储空间在什么时候分配?什么时候释放?

局部变量的存储空间在什么时候分配?什么时候释放?

2. 函数f3在调用与返回方式与函数f1与f2有何不同?

我们编译完成后,进入debug查看。

首先,我们执行到main函数处,然后开始单步执行。我们看到,每次单步执行时,涉及到取数据或者数据赋值的操作,debug都会显示出要操作的地址的地址和当前值。我们先来验证这个地址和值是否是正确的:

由于源程序中赋值n为0,不好验证是否正确,我们将n的赋值暂时改为n=10;我们验证如下:

(语句执行后DS:01a6处的值)

(语句未执行前DS:01a6处的值)

可以看到,n=10这条语句执行时,debug右侧显示出要操作的地址为DS:01A6,而程序执行后,DS:01a6处的值变为了0A,也就是说,被赋值的n的地址就是这里。由此,我们可以确定,debug单步执行时的地址和当前值是可信的。

我们依据每次单步执行时的debug值来确定每个变量的位置:

这里,n=0;全局变量的地址为:DS:01A6,DS段值为0B96。

执行到此,我们看到的依然是全局变量n的地址。

继续执行,我们进行到f2函数调用时。我们看到, f2(1,2);中的参数,2和1分别被压入了栈中。并且其参数压入的顺序是从右向左压入的。其段地址当然是SS。

在f2中参数被调用时,是用BP加偏移量的方式被调用。最后时,将结果给了AX。在这里变量C就没有再内存中赋值,而是直接用SI、AX来保存的过程值。而参数被调用的时候,用的段寄存器是SS。

另外在退出f2函数的过程中,我们看到:

在入栈是分别是AX(第二个参数)、AX(第一个参数)、BP、SI。而出栈是只将SI、BP出栈,并没有将AX出栈。在这个时候,参数就被释放掉了。

然后程序就返回了,所以我们在此处确定,函数返回值return是通过AX返回的。

将n赋值时,将变量AX的值赋值给了n。

这里也是n的赋值。

我们在编写这样一个程序:

从这里我们更清晰的看出局部变量是定义在栈段里。而且,我们看到在函数一被调用时就有sub sp+02,所以我们可以看出,在程序调用的时候就分配下了变量的空间。

所以:程序运行时,n的段地址是DS,A的段地址是SS,B的段地址是SS,C没有在数据段中,使用的是寄存器AX。

全局变量的存储空间在数据段中,局部变量的存储空间在栈段里,参数的存储空间在栈段里,函数的返回值储存在AX中。

全局变量的存储空间在执行到声明变量的语句时分配,程序结束后被释放。

局部变量在函数被调用时分配,在函数结束后被释放。

参数的存储空间在调用函数时分配,在函数调用结束后释放。

我们对比f1和f3调用的不同,发现:1.调用时,f1是call 偏移地址,而f3是call 段地址+偏移地址。2.返回时,f1是直接RET,f3是先RETF再RET。

2. 程序二:

我们编写如下程序:

问题:变量n与a的存储空间分配方式有何不同?

编译完成后进入debug跟踪。

我们看到,A在程序开始就被分配,A在偏移位置为0194的位置存放。而N则是PUSH进栈,这时候才在栈中分配的,在这之前是在SI中存放的。而且,C程序的编译器对程序作了优化,将语句简化。

我们做验证:

编写程序:

首先我们看到,C语言在实现的时候,并没有按照我们在C语言中的语句顺序这样一条一条的翻译,而在底层的实现方式是,将n=2与n++放在一起。且变量n放置在寄存器SI中。

3. 程序三:

我们编写程序如下:

问题:

1. 程序中所有变量的存储空间相邻么?tc2.0中,整型、字符型、长整数型数据的存储空间分别为多大?

2. 不同的数据类型对数据运算方式有何影响?

Debug查看main函数执行时a、b、c、a1、a2的地址。

观察知:a、b、c、a1、a2的地址分别为:0194、0196、0198、0199、019B。

我们看到他们之间的差值分别是:2、2、1、2。我们知道int型变量的大小是2字节,所以可以看到,这些变量是连续的。

而且我们还看到,在int和char行数据++的时候,都是用的inc指令,而long型的是用的ADD指令。说明不同类型的数据所对应的数据运算方法不同。

我们看到这里,long型的变量,先用add,在用adc指令。这是因为long型变量长度是4字节,而汇编没有四字节的加法。只能先将低两位进行加法,然后再将有高两位与溢出标志位进行加法(这里是因为低两位相加的时候有可能溢出,如果溢出,应该向高两位进1,,而adc指令是带溢出位进行运算的。这样以来就可以保证高两位的值是正确的)。这样来完整的完成一次四字节的加法。

我们查询TC的变量长度:

我们可以推测,a2后的变量的地址为019F。我们验证:

这里也说明不同类型的变量是连续的,也可以说明long占用了4个字节。

4. 程序四:

编写程序如下:

问题:变量a,b和他们的各个数据项的存储空间是如何分配的?

首先我们分析自定义数据类型:它是由一个int型,三个char型变量共同组成的。这样一个数据类型应该再内存中占用5个字节。而两个自定义数据类型的变量a、b,他们一个是全局变量,一个是局部变量。在分配时,应当一个在数据段中,一个在栈段中。

编译完成后debug查看结构体内的变变量来进行验证:

我们看到,在自定义的结构体stu类型的全局变量a中,其各个数据项的排列是连续的。且整个变量a都在数据段,所占用的长度为5个字节。

在在自定义的结构体stu类型的局部变量b中,其各个数据项的排列依然是连续的。且整个变量都在栈段里,所占用的字节为5个。并且,在这里我们发现,虽然是对栈段做操作,但是这里没有用PUSH指令,用的也是MOV指令。并且我们看到,在程序一开始的时候,

Sp就做了相应的修改,把b的空间分配出来了。

5. 程序五:

我们编写如下函数,看结构体变量是如何传递和返回的:

反汇编如下:

我们看到,在汇编中有LEA这条指令,LEA就是目标地址传送指令: 将一个近地址指针写入到指定的寄存器。格式:LEA reg16,mem16。

在程序中,我们看到这这样的调用,结合上下文我们可以确定是调用的func函数。我们看其反汇编的代码:

我们看到,程序将0b28给了ax,但是我们知道,ax存放子函数返回值的寄存器。那么ax中放的是自定义的数据类型的变量么?显而易见,ax是放不下5个字节的。那么,又没有可能是偏移地址呢?我们查看。

果然,在数据段中我们找到了自定义类型a的存放地址。也就是说,func函数返回自定义类型的变量是靠返回其在数据段中的偏移地址返回的。

我们查看f函数的语句,看到f函数的偏移地址为0256。

我们找到调用语句:

我们看到,F中是用栈传入的结构体变量。

所以,我们可以知道,自定义数据类型的变量在函数中返回时,是靠返回其在数据段中的偏移地址返回的。而其当做参数被调用时是借助栈机制传入函数的。

时间: 2024-10-12 19:57:31

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

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

1. 关于如下的程序,关于结构体的拷贝,拷贝是拷贝到内存中的什么地方? 我们进入debug进行反汇编,单步等操作跟踪查看.发现: 在main中,我们看到call 0266应该对应的是转跳到func处执行. 在这里,func赋值完成后,又call到了0B3D:13EA处,这里应该是其向内存中复制的函数.我们查看. 首先我们看LDS:从存储器取出32位地址的指令.和LES:LES( load ES)指令的功能是:把内存中指定位置的双字操作数的低位字装入指令中指定的寄存器.高位字装入ES寄存器. 我们

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

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

深入研究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): 获取指定索引位

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

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