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


    没有读过第一篇的读者,可以点击这里,阅读深入研究C语言的第一篇。

    问题一:如何打印变量的地址?

    我们用取地址符&,可以取到变量的偏移地址,用DS可以取到变量的段地址。

    1.全局变量:

    clip_image001

    clip_image003

    我们看到,这里的全局变量是在数据段中的。

    2.局部变量:

    clip_image004

    clip_image006

    我们看到,这里的局部变量是在栈段中的。

    问题二:研究main函数的偏移地址与源代码中main函数的定义位置之间的关系。

    我们打印函数的偏移地址,在打印的过程中我们可以发现:

    当程序编码如下时,程序运行的结果是:

    clip_image007

    clip_image008

    而将程序的f1函数和f3函数互换,程序运行的结果如下:

    clip_image009

    clip_image010

    可以看到,f1和f3的位置发生了改变,并且改变是相互颠倒了。

    我们还知道C语言中有这样的函数声明定义方式:

    clip_image011

    我们查看他的结果:

    clip_image012

    我们看到,在第一种方式下,f1—main的偏移地址依次增大;第二种方式中,f1与f3的偏移位置发生了互换。而在第三种方式中f1—main的偏移地址依然是依次增大的。从中我们可以得出结论:C语言程序的函数从01fa处开始。按照函数实现的顺序依次排列。在这里,函数从01fa处开始的原因是由于编译和连接的过程中,在我们编写的函数前添加了一部分固定长度的内容。

    问题三:

    阅读TC2.0完整目录下的“HELPME!.DOC”,解决以下问题:

    a) TCC.exe与TC.exe的区别?

    b) TCC.exe和TC.exe生成的exe文件有什么不同?

    首先我们参见文档中说明:

    TCC.exe与Tc.exe的区别:TC.exe是一个集成环境,质上是命令行编译器集成编辑器,链接器和调试器。而TCC.exe只是一个命令行编译器。

    TCC.exe和TC.exe生成的exe文件的不同:问:为什么。TC生成的EXE文件比由TCC.EXE生成的文件要大.在默认配置下TC.EXE生成的exe包含调试的信息。而TCC.EXE生成的没有。

    问题四:

    进一步通过debug观察两个程序分别通过TC.exe与TCC.exe生成的exe文件,理解TC.exe与TCC.exe对代码不同的优化。两个程序为:

    a) 仅打印“Hello World!”的程序;

    b) 拥有带参数的子函数的程序。

    首先我们看打印“Hello World!”的程序:

    源码:

    clip_image013

    看他们编译后的文件大小:

    clip_image015

    既然是比较不同点,我们就反汇编试试:开始的反汇编代码都相同,我们直接-U到01fa。发现:

    clip_image017

    左图为TC编译后,右图为TCC编译后

    这里出现了明显的不同:

    我们往前查看一些:

    clip_image019

    左图为TC编译后,右图为TCC编译后

    我们可以看出TC编译后的程序,在寄存器保护上比TCC编译后的更加全面。

    我们再看有带参数函数的程序:

    clip_image020

    分别编译并debug反汇编查看:

    clip_image022

    左图为TC编译后,右图为TCC编译后

    我们看到,左图比右图多更多的寄存器保护的语句。

    对于代码优化,TC更多的舍弃了效率和文件大小,来保证程序的安全性。而TCC更多的舍弃了程序的安全行,来生成精简的C程序,使得程序更加简短和高效。

    问题五:第2章中,程序需要打印函数的段地址和偏移地址,在command中直接运行和在debug中运行打印的段地址不同,偏移地址相同,这是什么原因?

    在这里,debug是用来调试程序的,他可以控制程序单步执行,并且查看程序运行中各种寄存器的状态。要做到这一点,debug肯定有他自己的控制方式。他需要将程序从debug内加载。而cmd运行程序,是系统执行的方式。他只需要接受系统的调用就可以执行。由于他们的运行方式不同,所以他们的段地址不同。但是,程序编译完成后,他的程序内的偏移地址就确定了(就像能我们打印main函数的偏移地址,说明这个地址是确定的)。而且每个程序都最大有64K的程序段和64K的数据段和栈段的混合段。所以在系统每次分配的时候,给每个程序都分配固定大小的但是位置不同的内存(一个64K的程序段,一个64K的数据段和栈段的混合段),即栈地址固定,偏移地址从0000-FFFF的内存。

    问题六:第2章中,同时用多个dos窗口加载程序,打印所得的段地址和偏移地址都相同,这是什么原因?

    在《汇编语言》书中,附注1的内容介绍了Inter系列微处理器的3种工作模式。

    (1) 实模式:工作相当于一个8086。

    (2) 保护模式:提供支持多任务环境的工作方式,建立保护机制。

    (3) 虚拟8086模式:可以从保护模式切换至其中一种8086工作方式。这种方式提供使用户可以方便的在保护模式下运行一个或多个原8086程序。

    而我们的windows是基于80386的。我们可以这样轻松的工作,开两个窗口,一个是工作于保护模式的word,一个是工作于虚拟8086模式的DBASE。

    也就是说我们现在的command是虚拟8086的模式下工作的。既然是虚拟的,两个command之间就没有什么关联。两个command之间也就不会共用一段真实的内存(他们的内存是虚拟出来的)。所以打印所得的段地址和偏移地址都相同。

    问题七:我们使用基于tc2.0的精简开发环境进行综合研究是为什么,这样做对我们有什么帮助?

    使用精简的开发环境,可以减少我们的所面对的问题,集中精力解决我当前所要研究和解决的,C语言的基本问题。并且,这样做,我们可以更加深入的研究C程序在编译连接中所用到的深层次的、必须需要的过程。

    问题八:语句printf(”%x %x %x ”, main, &main, *main);打印的结果都是同一个值,试着解释原因。

    我们编写这样一个程序:

    clip_image023

    clip_image024

    clip_image025

    clip_image026

    这可以说明,printf是可以显示常量的。并且&和*常量,显示的都是常量的值。

    我们知道,在汇编编译连接的过程中,其实是进行了两次,第一次是编译各种机器码,这是并不知道标号是在什么位置,第二次是在第一次完成后再次将翻译编译标号的地址。

    C语言也有可能这样,main被翻译成了main函数所在的偏移地址(一个常量,这个常量的值是由第一次编译确定的)。这样,也就可以说明(long)main出来后显示的是段地址偏移地址的问题。

    问题九:使用更多的方法完成打印main函数偏移地址

    我们可以使用这样的方法打印:

    clip_image027 clip_image029

    我们还有这样的办法打印:

    clip_image030

    clip_image031

    问题十: tcc精简环境编译生成的exe文件中的程序可有两个最大为64k的段,那么当我们需要的代码段或者数据段超出64k怎么办?

    我们拿数据段做验证,首先计算数据段的大小:

    我们知道,一个int型变量在内存中占两个字节,而数据段和栈段共用一个段。段的大小是64K,我们计算64K的数据段能存放多少个int型数据。答案是最多存放32767个int型。我们编写程序如下:

    clip_image032

    我们故意将数组的数量设为32768,结果发现在编译的时候TCC报错:说我们定义的数据超出范围。这说明在编译的时候,程序会自动检查你编写的程序的数据长度,如果超出,则编译不通过。(注,此时没有TLINK.exe文件)

    clip_image034

    我们将数组的值设为32767,然后编译,其错误信息如下:

    clip_image036

    这说明TCC的编译工作已经正常完成,但是由于没有TLINK.exe文件,无法生成.exe。我们放入TLINK.exe,再次编译。

    clip_image038

    我们发现其仍然提示段已经超出64K。直到数值粗略改到32500左右时,不再报错。

    这是为什么呢?这里提出两种可能:

    1. 编译连接器自动给程序保留栈:因为程序在执行的过程中不可避免的要用到栈,所以在连接的时候,编译连接器自动给程序保留了一部分栈内存。当它发现这部分内存加上本身定义的数据超过了64K,就报错了。

    2. 编译连接器在编译连接过程中向数据段写入了内容:程序连接的过程,是TLINK.exe将c0s.obj、cs.lib、emu.lib、maths.lib中的相关代码与程序的代码连接到一起生成.exe文件。在这个过程中有可能向程序段中加入了数据。导致程序段超出64K。

    对于两种猜想我现在还没有想到非常巧妙的方法证明,只能暂时这样猜想。

    而:当我们的程序不得不大于64K时。我们可以向系统申请内存,或者使用没人使用的安全内存,将数据或者程序写入这段内存中,在从这段内存中使用数据或者调用代码。

  • 相关阅读:
    C#之类和对象
    uml中关联与依赖
    uml中的各个关系
    数据挖掘聚类算法分类(转)
    (转)Client http persistent connection limit
    牛客网NOIP赛前集训营提高组(第七场)Solution
    训练题表
    CF1000赛后总结
    UVA3983 Robotruck 题解
    CF1034A Enlarge GCD
  • 原文地址:https://www.cnblogs.com/shandianlongxiao/p/4027365.html
Copyright © 2020-2023  润新知