购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.5 编程思想(等差数列求和)

编写这一部分的目的,是锻炼读者编码的思维。从简单的数学计算,找出原理,再试着去编写相应的代码,从简单到复杂,循序渐进。动手写代码是极其重要的,笔者见过不少大学生,在学习计算机课程时,理论知识都不错,但被要求实际编写成代码时,就写不出来了,不知道从何下手。出现这种情况,大都是因为太少实际动手写代码,没有形成写代码的习惯,所以一下子很难写出像样的代码来,于是为了应付作业,开始抄袭。通过这部分的学习,希望读者能从中得到启发,发掘写代码的潜力。在笔者看来,写代码比学习一些数学理论知识要容易得多,那么既然理论知识都可以掌握得很好,又有什么理由把写代码当作一件难事呢?

“等差数列求和”是指什么?先简单介绍一下,等差即相等的差距,数列即数字的列表,求和就是把列表之中的数字累加。比如自然数数列1,2,3,4,5,第一个数和第二个数之间的差距是1,第二个和第三个的差距也是1……任意相邻两个数字之间的差距都是1,这样的数列就是等差数列,这个差距1则被称为公差因子。

自然数从1到100的求和是数学史上一个著名的问题。人们最初想到的求和方法大概都是直接从1累加到100,这是最直接最基本的方法了,但如果我们要求的是从自然数1累加到1 000、10 000、100 000……呢?显然,随着累加范围不断扩大,求和会变得越来越困难,那有没有捷径可以走呢?现在让我们先观察一下这些自然数:1,2,3,…,98,99,100,其中是不是有什么规律呢?是的,如果我们把这100个自然数分成50组,每组都是首项和对应尾项的组合,即1和100,2和99,3和98,……,50和51,那么我们会发现每组的相加结果都等于101,也就是说,从自然数1到100的累加可视为50个101相乘,即(1+100)+(2+99)+…+(50+51)=101×50=5050。

这个方法据称是由德国数学家高斯最早提出的,即倒序相加法,后来经过演变得到了等差数列求和公式。根据等差数列求和公式,当公差为1时(公差即相邻两个数之间的差值), 。其中,a 1 为数列首项的值,a n 为数列末项的值,n为待求和自然数的总数。对于1到100的求和问题而言,a 1 =1,a n =100,n=100,因此,1到100的和为

以上是数学上的原理,那么我们如何应用到实践中,让计算机去帮助我们解决这些问题呢?我们可能首先会想到的是在Visual Studio中创建一个控制台工程,然后用C语言写代码实现。下面两个函数都实现了自然数从a 1 到a n 的累加求和。

我们对ASR函数和Accumulate函数做一对比。从计算机运行的效率上看,ASR函数肯定比Accumulate函数要胜出很多,但在便于理解方面则要差一些。在实际学习过程中,如果把累加的方法编写成函数,有些学生就会先想用算法解决,比如“等差数列求和”,然后再编写ASR,这样的方式对于编码成熟度较高的学生来说是很好的,但对于刚刚学会编程的学生,则应先学会编写Accumulate,然后再改进到ASR。总体思想是,先学会快速解决问题,如果需要,再改进解决问题的方法。

在Visual Studio中,我们能很容易地编写代码使其运行,最终获得想要的结果。因为Visual Studio是运行在Windows操作系统中的。假如现在我们脱离操作系统提供的运行环境,把这些代码放到一台“干净”的计算机中运行,那又需要怎么做呢?在解决这个问题之前,我们先来了解一些基本的计算机知识。

计算机开启电源后,经过一系列BIOS(Basic Input/Output System)自检和硬件初始化过程,之后BIOS将控制权交给加载引导驱动器。加载引导驱动器会从(磁道0,柱面0,扇区1)读取1扇区(512字节)数据到物理内存地址0x7c00(约定的物理内存地址),这个独特的扇区叫作主引导记录(Master Boot Record,MBR)。加载引导驱动器将控制权传递给MBR。为了让我们编写的代码有机会运行,我们把代码搬到MBR中。由于MBR运行的是十六进制机器码,所以我们首先要把C语言代码编译成十六进制机器码。

通过C语言代码把握函数的大致思路后,现在我们转入对相应汇编代码的学习。如何把上面的C语言函数转换成NASM汇编函数呢?如果你有比较好的汇编代码编写功底,可以根据思路重新设计代码。如果觉得麻烦,或者暂时不知如何写一个汇编函数,那么可以通过使用Visual Studio创建一个控制台工程,复制上述C语言函数到建好的工程里,然后编译,生成EXE文件后,按F10键进入调试状态,转到反汇编页面,把属于C语言函数的汇编代码复制出来。除此之外,我们也可以使用IDA工具打开生成的EXE,从IDA解析出的汇编中,复制C语言函数的反汇编代码。由IDA生成的汇编代码更容易整理,通常用这种方式整理出来的就是NASM代码。

MBR代码是我们编写的所有代码中第一个运行的代码块。下面是ASR.asm的一段MBR代码:

上述MBR代码的目的是调用两个版本的函数计算1到100的累加和。运行结果在函数返回后放在eax寄存器中,为了查看这个结果,读者需要使用Bochs调试跟踪。

在Visual Studio中先整理出反汇编代码,在ASR函数上按F9键下源码断点,按F5键运行,中断下来后,转到反汇编窗口,在该窗口中单击鼠标右键,从弹出的菜单中,取消对一些不需要的菜单项的勾选,特别是“显示符号名(S)”,否则显示的代码会不一样。最终如图1.12所示。

图1.12

这是我们原样复制得到的汇编代码,但这些代码是不能直接编译的,还需要整理一下,去除一些不必要的汇编代码,比如sub esp,0C0h,它的用途是为局部变量开辟栈空间,然而分析这段反汇编代码,并没有使用到局部变量,所以不需要栈空间。除此之外,下面几行代码也是不必要的:

整理完成的ASR汇编代码如下:

对于mov eax,dword[ebp+6]这样的代码,实际上隐藏了ss段寄存器,原型应该是ss mov eax,dword[ebp+6],有些汇编形式为mov eax,dword ss:[ebp+6]。

在ASR函数中,前后两个方框中的汇编代码是比较经典的函数栈框架,对于这类栈框架模型,ebp在函数中是不会改变的。一旦有入栈操作,esp就会发生改变(esp=esp-n),为了获取传入的参数,如果使用esp引用,每次都需要重新计算地址,比较麻烦,所以通常使用ebp引用传入的参数位置。

根据MBR的代码我们知道,栈顶设置在0x7c00,push dword 100后(按照调用约定,这是第二个参数),参数值100存放在0x7c00-4=0x7bfc(记住这个地址)。push dword 1后(按照调用约定,这是第一个参数),参数值1存放在0x7bfc-4=0x7bf8(这个地址也要记住)。栈顶现在在0x7bf8,call ASR又会push下一条指令的地址到栈中,因为运行在16位实模式下,push的地址是两字节,于是栈顶在0x7bf8-2=0x7bf6。ASR入口再push ebp,当前栈顶在0x7bf6-4=0x7bf2,mov ebp,esp把栈顶位置保存在ebp中,这样之后,在ASR函数内引用参数时都使用ebp指定的位置,因为ebp不会再改变(直到函数退出时从栈中还原之前的值)。

下面我们看一下如何通过ebp来引用参数的地址。现在ebp的值是0x7bf2。我们知道第一个参数的地址是0x7bf8,与当前的ebp地址相比,它们相差了6字节位置,因此“ebp+6”就是第一个参数的位置,取第一个参数的值时就用“[ebp+6]”。那么第二个参数在哪里呢?我们从上面知道第二个参数的地址在0x7bfc,对比当前ebp地址,它们相差了10字节位置,所以“ebp+10”就是第二个参数的位置,取第二个参数的值时就用“[ebp+10]”。明白了参数的关系,理解ASR的汇编代码就简单了。

在函数内,像上面这样使用ebp引用传入的参数地址,形式是ebp+n,离栈顶越近的参数n值越小,离栈顶越远的参数n值越大。函数的返回值放进eax。这种函数栈框架实际上就是随后我们要说的stdcall和cdecl函数调用约定。

下面看Accumulate的汇编代码。

Accumulate的汇编代码比ASR要多,而且使用了函数栈内存,引用栈内存也是通过ebp寄存器,不过栈内存是在函数内的,所以地址不再是ebp+n,而是ebp-n。然而我们从上面的汇编中也看到了[ebp+i],不过这个i,定义为i equ-8,实际是负数,所以[ebp+i]等于[ebp-8]。现在我们应该明白,C语言中的函数内定义的局部变量,实际上是在函数栈中分配的空间。执行流程一旦从函数返回,栈内的数据就不应再被引用了。

双击ASR.bat就可以编译ASR.asm,生成ASR.img,并发起Bochs调试。Bochs在f000:fff0第一次中断后,下断点b 0x7c00,再输入c,在执行流程来到0000:0x7c00后,输入n单步跟踪执行,如图1.13所示。

图1.13

跟踪到call.+32<0x00007c36>后,使用x/4 0x7c00查看入栈的两个参数值。第一个参数是0x00000001,第二个参数是0x00000064(十进制值是100)。输入s可进入ASR函数内观察,输入n则步过ASR执行,输入r可查看ASR返回的结果,结果存放在rax寄存器中。我们观察到ax的值是0x13ba,十进制值就是5050,这就是1累加到100的结果。继续单步跟踪,可以观察到Accumulate的运行,这部分操作留给读者自行完成。 tIkfe0OXbcK8g6d4KJLq/3Q2mjnTDyRUlVOSWkeYr55MTugqi30Sq3vysm9LhuQo

点击中间区域
呼出菜单
上一章
目录
下一章
×