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

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

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

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

1.全局变量:

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

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

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

2.局部变量:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

我们查看他的结果:

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

我们看到,在第一种方式下,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!”的程序:

源码:

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

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

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

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

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

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

这里出现了明显的不同:

我们往前查看一些:

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

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

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

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

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

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

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

左图为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\n”, main, &main, *main);打印的结果都是同一个值,试着解释原因。

我们编写这样一个程序:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

上一篇:js打印对象(object)


下一篇:一个经典的PHP加密解密算法