山东大学操作系统学习笔记:第3.1讲程序的结构-简单的程序

第3.1讲:程序的结构-简单的程序

可执行文件 & 程序的装入

image-20240918151520355

  • .rwdata(读写数据段): 存放程序中的含初值常量。这些常量在程序运行可以修改。

  • 零初始化数据段(.zidata/.bss - Block Started by Symbol):存放程序中的不含初值(初始化为0的)可修改常量。

  • 堆段: 存放程序的堆,也即动态分配内存时内存的来源。

  • 栈段: 存放程序的运行栈,以供过程调用时保存和恢复上下文。

可执行文件头

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


最后,在将控制权交给程序时,将PC指向.text段,将SP指向.stack段。


编译器、汇编器、链接器、调试器与解释器程序

image-20240918151901715

工具链

image-20240923192533286

外部符号引用

在一个目标文件中可能引用了其它目标文件的内容。由于编译 器一次生成一个目标文件,因此并不知道那些引用的内容的地 址在哪。况且,自身在内存中的位置也还没确定,因此所有对 符号的引用(全局变量访问、函数调用等等)的具体地址都只 能推迟。

这个时候需要链接器进行操作:链接器会先收集全部目标文件的符号,然后给每个符号分配地址。在地址确定后,反过来补全程序中对这些符号地址的引用。

image-20240923194210240

  • 直接回填法

image-20240918152955321

  • 间接地址法

image-20240918153038187


调用者向被调用者传递一个或多个参数

参数的传递有三种方法:

(1)通过全局变量传递(需要在数据段声明)

(2)通过栈传递(记得设置SP和SS)

(3)通过寄存器传递(寄存器数量有限)

下面是利用栈传递的情况:

image-20240923222447116

GPT解释:

这张图展示了两个 C 语言函数和它们对应的汇编代码,并且结合了栈帧的概念。我们分别解释一下左侧的 main 函数和右侧的 subp 函数及它们的汇编指令。

左侧代码:main 函数及其汇编指令

C 代码:
int main(void) {
    ...
    val = subp();
    ...
}
汇编指令解释:
  1. MOV BP, SP:保存栈指针到基址指针 BP。栈帧基址设置为当前栈顶指针(SP)。在函数调用中,BP 通常用于引用局部变量和参数。

  2. SUB SP, 2:在栈上分配 2 个字节的空间,可能是给局部变量 val 分配的空间。

  3. CALL FAR PTR SUBP:调用远程子程序 subp()。调用指令会将 IPCS(指令指针和代码段寄存器)压入栈中,然后跳转到 subp 函数的地址。

  4. MOV AX, [BP-2]:从栈中取出 subp 函数返回的值,BP-2 指向栈中的返回值,将其加载到寄存器 AX

  5. MOV VAL, AX:将 AX 中的值(subp 返回的值)存储到变量 VAL 中(对应 C 代码中的 val)。

  6. MOV SP, BP:将栈指针恢复到基址指针 BP 的值,清理栈帧。

右侧代码:subp 函数及其汇编指令

C 代码:
short subp(void) {
    ...
    return value;
}
汇编指令解释:
  1. MOV DX, VALUE:将 VALUE 的值(subp 函数要返回的值)移动到寄存器 DX

  2. MOV [BP-2], DX:将寄存器 DX 中的值存储到栈中 BP-2 的位置,这是给 main 函数的返回值留出的栈空间。

  3. RETF:返回远程调用,将 IPCS 出栈并恢复程序的执行,返回到 main 函数中。


栈帧分析:

  • main 函数调用 subp 时,栈中会保存当前的指令指针 (IP) 和代码段 (CS),从而实现函数调用的跳转。
  • subp 函数会将返回值放到栈中,main 函数再从栈中读取返回值并赋给 val
  • 栈指针 (SP) 和基址指针 (BP) 的配合用于管理函数调用时的参数传递和局部变量。

总体上,这段代码展示了如何通过栈来处理函数调用和返回值。


补充知识:

MOV指令,能实现以下操作:

  • CPU内部寄存器之间数据的任意传送(除了码段寄存器CS和指令指针IP以外)。

  • 立即数传送至CPU内部的通用寄存器组(即AX、BX、CX、DX、BP、SP、SI、DI),给这些寄存器赋初值。

  • CPU内部寄存器(除了CS和IP以外)与存储器(所有寻址方式)之间的数据传送,可以实现一个字节或一个字的传送。

  • 能实现用立即数给存储单元赋初值。

img

在汇编语言中,bp和sp分别指代以下内容:

  • sp(栈顶指针):指向栈顶地址,与SS相配合使用,用于访问栈中的数据。
  • bp(基址指针):指向栈帧的底部,一般称之为栈底指针,用于定位物理地址
    • BP叫做栈框指针,因为通过它就能找到与该过程相关的整个调用栈。调用栈包括参数,临时变量,还有返回时的IP

栈的四种类型

image-20240923222802997

递增栈的优势:便于判断栈是否溢出


Q:参数和返回值的传递都有多种方法。那么,调用一个函数时, 具体采取哪种方法,以及那种方法具体是怎样实现的?

A:被调用者和调用者都遵守的变量和返回值的传递规则。具体怎 样约定,是随心所欲的。但是,一旦这个标准确定下来,就必 须在整个项目中遵循,否则就会乱套。常见的8086(包括后续 的i686扩展)使用的传值约定大体如下。

image-20240923222955147


Q:为什么绝大多数调用约定都把参数放在栈上?为什么不使用寄 存器传递所有的参数,或者使用变量传递所有的参数呢?

A:栈的大小比较大,因此传递的参数可以比较多,准确地讲数量 是不受限制的。此外,栈传递参数非常好理解,一般是把当前SP 赋给BP,然后将参数列表从右往左依次入栈就可以了。被调用 者通过BP指针就可以寻址各个参数,调用完成后要将SP指针恢 复到原状也只需要将BP再赋给SP,非常方便。之所以8086的BP 寄存器被指定隐含SS段寄存器,就是为了方便栈传参。


Q:使用栈传递参数太麻烦了,要保存栈框,还要恢复栈指针。那 么,在小型程序中,能否在数据段定义几个变量传递参数呢?

A:(概括一下)这样的话不能使用任何形式的递归调用

Q:直接传参法和间接地址法的优劣比较(课前小测)

A:两者都是对于程序中符号地址的引用进行补全(链接器的操作)

  • 直接传参法:运行时速度快,但是不便于在运行时修改地址
  • 间接地址法:运行时速度满(相当于多进行一次指针转换),但是便于修改地址

寄存器保护约定

  • 调用者负责

  • 被调用者负责:更容易做到权责一致,但是会产生不必要的PUSH和POP

  • 混合制负责:部分寄存器由调用者负责保存,另一部分寄存器则由被调用者 负责保存。下例中,AX、BX由调用者负责保存,CX、DX则由被 调用者负责保存,且AX~DX中均含有调用者的有效数据。

    • 优势:一方面有利于生成尽量少的PUSH和POP,减少代码量,并方便被 调用者内部决定保护哪些寄存器(若被调用者也不使用那个寄存器,则不管便可),另一方面也允许在每次调用时定制保护列表,触碰尽量少的寄存器。
      • 一方面允许编译器进行高度复杂的优化,另一方面也方便人手写汇编。

栈框恢复约定:

  • 调用者负责
    • 优势:可以最少化栈框调整。如果一个过程被连续调用两次,或者在 循环中被反复调用,或者尾递归,那么也许调整一次SP就足够了, 已分配的栈框本身可以重新填值并复用。另外,栈框怎么调整 是调用者说了算,因此调用者可以更灵活地处理每一次调用。 这在那些**可变参数函数(如C语言printf)**中非常有用。
  • 被调用者负责
  • 混合制负责
调用者负责:

image-20240924203823744

这段栈框恢复代码的作用是保存和恢复调用子程序时的栈帧。下面是对每行代码的逐步解释:

  1. PUSH BP:将基指针(BP)的值压入栈中。这样做的目的是保护当前栈框,以便在函数返回时能够恢复。

  2. MOV BP, SP:将当前栈指针(SP)的值赋给基指针(BP)。这一步建立了新的栈帧,使得接下来的局部变量和参数可以通过BP来访问。

  3. PUSH VAR:将局部变量(这里用VAR表示)压入栈中,通常用于为子程序传递参数或保存局部变量。

  4. CALL SUBP:调用名为SUBP的子程序。在调用时,返回地址会自动压入栈中,以便子程序执行完毕后能够返回到正确的位置。

  5. MOV SP, BP:在子程序执行完毕后,这一行将栈指针(SP)恢复到基指针(BP)的位置。这意味着局部变量和参数已经不再需要,可以将栈指针恢复到上一个栈框的状态

  6. POP BP:从栈中弹出之前保存的BP值,恢复到原来的基指针。这一步是栈框恢复的最后一步,确保在函数返回后能够正确地访问调用者的栈框。

总结来说,这段代码通过保存和恢复BP值以及SP值来管理栈帧,使得在调用子程序时能够正确处理局部变量和参数,并在返回时恢复到原来的栈状态。

被调用者负责:

image-20240924204929515

这段代码展示了被调用者负责的栈框恢复约定,主要分为主函数和子程序两部分。以下是对代码的详细解释:

主函数部分

  1. PUSH BP:将当前基指针(BP)压入栈,以保护调用者的栈框。

  2. MOV BP, SP:将当前栈指针(SP)的值赋给基指针(BP),建立新的栈框。

  3. PUSH VAR1:将参数(VAR1)压入栈中,以便传递给子程序SUBP。

  4. SUB SP, 2:在栈中为局部变量分配空间,这里分配了2个字节。

  5. CALL SUBP:调用子程序SUBP,返回地址会被压入栈。

  6. POP BP:从栈中弹出之前保存的BP值,恢复调用者的栈框。

子程序部分 (SUBP)

  1. 注释:指出被调用者需要弹出所有局部变量和参数。此处有两个参数和一个局部变量需要弹出。

  2. MOV AX, [BP-2]:通过基指针(BP)访问压入栈中的第一个参数(VAR1)。因为参数相对于BP的位置是负偏移。

  3. MOV [BP-4], CX:将寄存器CX的值存储到第二个局部变量的位置(BP-4)。这说明此子程序有一个局部变量。

  4. RET 4:从子程序返回,同时清理4个字节的栈空间,实际上是弹出之前传递的参数。此指令确保在返回时,栈指针恢复到调用子程序之前的状态。

总结

这段代码体现了被调用者负责的栈框恢复约定:在子程序中,必须处理自己的局部变量和参数,并在返回时清理栈空间,以保证栈的完整性。主函数负责设置栈框和调用子程序,而子程序则负责自己的栈管理。

序言,尾声

image-20240924165538888

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值