我们知道,大部分计算机程序都可以被分为若干具有特定参数、可以互相调用的子程序。
当一个子程序调用另一个子程序时,前者需要将调用参数传递给后者,同时还需要将自己的局部数据(寄存器上下文、局部变量等)妥善存放,保证在后者返回时前者可以继续执行原本的任务。
调用栈(下简称为「栈」)是计算机程序用于存放子程序信息的内存空间。
我们知道栈是一种后进先出的数据结构,因而特别适合用于维护嵌套结构的信息——后进(内层)的元素先出(结束),且子结构和栈中已有内容不会相互影响。
因此,计算机程序利用调用栈的方式也类似:
容易发现,栈的性质保证了:
本题的任务是模拟一个具有调用栈的抽象计算机的运行。
该计算机支持注册一个主程序(无参数)和若干有特定参数的子程序。
为了简化,我们只允许两种类型的参数,即数字 int 和字符串 str。其中数字范围和 C++ 中的 int 一样,字符串长度为 3,且仅由英文小写字母构成。
主程序不需要声明,子程序的声明是 函数名(参数列表),比如:
some_subroutine(int, str, int)
调用的时候遵守一样的格式 some_subroutine(5, arg, 10)。
特殊地,子程序可以有不定参数,例如 va_subroutine(...),这表示可以有不定个数的参数,每个参数都可以是两个合法的类型之一,比如这样调用它:
va_subroutine(5, arg, 6, 9, arg)
混合不定参数和固定参数也是可以的,但要保证不定参数出现在参数列表的最后,例如 mix_subroutine(int, ...),
不定参数对应的个数可以是零个,比如这些是合法的调用:
va_subroutine()
mix_subroutine(5)
当然,主程序和子程序的主体部分是由若干指令组成的,每个子程序的指令从 开始按顺序编号,计算机默认从主程序的 号指令开始运行。
接下来我们一边梳理该计算机(或其上的程序)具有的功能,一边说明该计算机支持的各个指令类型。
首先,该计算机具有一个单字节暂存器,程序可以操作/输出暂存器的值:
ADD BYTE),把暂存器的值加上某个字节,BYTE 格式如 0x0a。SET BYTE),把暂存器的值设置为某个字节,BYTE 格式如 0x0a。ECHO),按十六进制格式(如 0x0a)输出暂存器的值。其次,该计算机具有一个大小为 字节的栈空间,按字节从 到 编址。该计算机内部还维护该栈的栈顶位置(初始为 0,表示栈为空,栈顶为 x 表示栈空间为 [0, x)),可以通过指令来以字节为单位压栈和弹栈:
POP),弹出栈顶到暂存器中。PUSH),往当前栈压入暂存器上的字节。和上文所述类似,该计算机能够维护所有还未结束的子程序栈帧范围,因此支持如下指令:
LOAD),把暂存器中值对应的位置读入暂存器。READ POSITION),读取栈上的某个位置到暂存器中。WRITE POSITION),将暂存器中的内容写入栈帧的某个位置(不能写到别的栈帧上)。PRINT POSITION),从某个位置开始打印字符串,遇到 \0 停止。以上的位置,都是相对于栈帧而言的,例如 READ 0 表示读取当前子程序栈帧的第一个字节到暂存器中。
另外,暂存器在表示位置时视为一个有符号整数。
我们说过栈在任意时刻都是从主程序到当前子程序的调用链上各个子程序栈帧的排列,因此该计算机支持在通过遍历栈上的所有栈帧输出调用链:
STACKTRACE),在 verbose 模式下输出栈追踪信息,否则什么也不做。栈追踪信息是表示调用链信息的特定格式,我们后续再谈及。
另外,该计算机支持分支和跳转:
JUMP OFFSET),跳转到程序的 BYTE 号行,行号从 开始计数。B COMMAND),如果暂存器的值为零,就执行某指令。该计算机还能够在任何时刻终止运行:
TERMINATE),无论如何,终止程序运行并输出 TERMINATED,并在换行后输出栈追踪信息。显然该计算机还需要支持子程序间的调用和子程序的退出,于是:
CALL CALL_BODY),根据 CALL_BODY 执行函数调用。LEAVE),退出当前的子程序,回收当前子程序栈帧,特别地,如果执行这条指令的是主程序,则等同于执行 TERMINATE。注意在调用时需要压入子程序参数,我们按照从后往前的顺序压入子程序的所有参数。
对于两种参数,压入的方法是:
int 四字节小端压入栈,比如一个整数 0xff50fc50,按照 0x50,0xfc,0x50,0xff 的顺序压栈。0x00,共计四个字节。当然程序可能会出现异常,异常包括:
ILLEGAL_ACCESS),读取、写入或打印字符串时,访问了非法的位置,比如 WRITE 写到了其他栈帧上,READ 读取到了超出栈边界的位置。FRAME_OVERFLOW),在栈帧为空时执行 POP。STACK_OVERFLOW),使用的栈空间总量超过了上限(可能由 PUSH 或 CALL 导致),或者在栈为空时 POP(其实这是不可能发生的,因为它必然和上个错误同时发生,但我们规定优先触发上一个异常)。BAD_PARAM),调用函数的参数不符合参数列表。NOT_PRINTABLE),PRINT 指令处理到了一个无法打印的字符。请不要执行将要触发异常的指令。特别地,如果 PRINT 指令读取越界,请把它越界以前的字符正常打印出来。
在发生异常之后,请在打出异常名之后(需要在末尾换行)结束程序运行,并输出栈追踪信息。
作为一个玩具程序,我们不希望模拟器执行太多条指令,因此,模拟器将把它执行的第 条指令自动替换成 TERMINATE 以结束程序。
同样地,我们也不希望模拟器递归太多层,因此,模拟器将把第 层函数(main 是第 层)的 call 指令替换成 TERMINATE 以结束程序。
最后,我们来说说栈追踪信息,我们针对一个范例来说明:
========STACKTRACE:
reg: 0xa5
=========
main()
5> CALL calladd(5)
frame_range [0, 4)
stack_frame:
05 00 00 00 ....
=========
calladd(5)
0> CALL printparams(3, cal, lad, dxx)
frame_range [4, 20)
stack_frame:
64 78 78 00 6c 61 64 00 dxx.lad.
63 61 6c 00 03 00 00 00 cal.....
=========
printparams(3, 0x006c6163, 0x0064616c, 0x00787864)
0> STACKTRACE
frame_range [20, 20)
stack_frame:
=========END
========STACKTRACE:,然后紧随第二行输出暂存器的值。========= 作为开头:
int 转十六进制输出。num> 子程序此时正在运行的指令,其中 num 是该指令在子程序的编号。注意异常发生时,认为子程序正在执行该指令。此外,被替换为 TERMINATE 的指令在这里需要显示为原指令。.,注意 (空格,0x32)是可显示字符。),注意不满八个字节的情形要将输出的字符起始和满八个字节的情形对齐。========END。事实上这个追踪信息已经构成了这个抽象计算机目前运行状态的快照。
到这里我们已经介绍完了该计算机的全部功能,接下来我们将给出所有主程序,子程序的信息和计算机的栈大小,以及它是否处于 verbose 模式,你的任务是模拟它的运行。
第一行一个数,表示计算机是否处于 verbose 模式。
第二行三个整数 ,分别表示子程序数量,栈空间的总大小和主程序的指令条数。
接下来 行,按顺序给出了主程序的指令,一行一条。
接下来是 个子程序的信息,第 个子程序的信息如下:
第一行是子程序的声明。
第二行一个整数 ,表示子程序的指令条数。
接下来 行,按顺序给出了该子程序的指令,一行一条。
按照题意输出。
0
1 4096 11
CALL printparams(7, goo, dby, epr, obl, ems, olv, ing)
SET 0x7e
PUSH
SET 0x20
PUSH
SET 0x20
PUSH
SET 0x00
PUSH
CALL printparams(8, goo, dby, epr, obl, ems, olv, ing)
LEAVE
printparams(int, ...)
33
STACKTRACE
READ -4
PUSH
SET 0xfc
PUSH
READ 0
B JUMP 22
ADD 0xff
WRITE 0
READ 1
ADD 0xfc
WRITE 1
ADD 0x00
LOAD
PUSH
READ 1
ADD 0x01
ADD 0x00
LOAD
PUSH
READ 1
ADD 0x02
ADD 0x00
LOAD
PUSH
ADD 0x00
READ 0
JUMP -21
SET 0x00
PUSH
PRINT 2
STACKTRACE
LEAVE
goodbyeproblemsolving
goodbyeproblemsolving~
TERMINATED
========STACKTRACE:
reg: 0x00
=========
main()
10> LEAVE
frame_range [0, 4)
stack_frame:
7e 20 20 00 ~ .
=========END
0
0 4096 16
SET 0x50
PUSH
SET 0xfe
PUSH
STACKTRACE
READ 0
ADD 0x01
WRITE 0
READ 1
ADD 0xff
WRITE 1
B JUMP 2
JUMP -8
ECHO
STACKTRACE
LEAVE
0x00
TERMINATED
========STACKTRACE:
reg: 0x00
=========
main()
15> LEAVE
frame_range [0, 2)
stack_frame:
4e 00 N.
=========END
对于全部数据,保证 。
保证输入的任何一行长度不超过 。
保证输出总长度小于 。
保证子程序声明是合法的。
保证每次跳转都是合法的。
保证不会调用不存在的子程序。
保证子程序调用是符合语法的且不出现多余空格,且每个逗号后固定有一个空格,保证每个参数一定是两种类型之一的合法取值。
保证每个子程序的最后一条指令一定是 LEAVE。
样例解释:简单的加法程序,0x50 + 0xfe = 0x4e。
其他样例详见附加文件。