简单的前言
虽然这里的日期是 2024-04-03,但是我写这篇博客的时候其实是 2025-02-18,写 2025-02-17 那篇博客的时候,提到 polyglot 相关的内容,这部分内容其实是上上个学期的计算机系统的讨论课的产出,于是想着把沉睡在 …/Downloads,…/Courses/CS24Spring,小组群文件等目录里的讨论课材料里有点意思的部分翻出来晾晾,所以这一页博客出现在你的眼前.
我看到相当多的说法说 bash 的语法很不舒服,然而我在接触到这种说法之前,已然习惯了 bash 的语法,“我比流言蜚语先认识你”??😶🌫️
这篇博客相关的讨论课的题目是——对于 C 中的 switch-case
,编译器的行为如何,即 case
呈现什么样的分布(连续/不连续,间隔大小)时,编译器将生成跳转表. 既然要探讨不同 case
对应的情形,那么首先要生成不同 case
对应的 C 源文件,这个事情疑似有点机械了,那么考虑用自动化的脚本去完成. 我们对这个脚本的预期是 generator -b 10 -s 2 -d dest_dir -f file_name
将产生分支数量(-b
)为 10,分支间隔(-s
)为 2,存放目录(-d
)为 dest_dir,文件名(-f
)为 file_name.c,有了单个文件的生成器,我们可以再写一个生成器来调用这个生成器,形成一批分支数量 / 分支间隔不同的 C 源文件.
命令行参数捕捉
命令行最显而易见的好处在于,你可以通过选项来控制命令的具体行为,比如 cat
会为你呈现文件内容,而 cat -n
可以帮你在文件内容旁边打上行号,那么如何捕捉命令行参数——getopts
:
while getopts ":b:d:f:s:" opt; do
case $opt in
b)
branch=$OPTARG # number of switch branches
;;
:)
echo "Option -$OPTARG requires an argument."
;;
?)
echo "Invalid option: -$OPTARG"
;;
esac
done
最简单的元编程
这里的元编程指用代码生成代码,它可以很复杂,然而这里只采取一种最简单的观点——代码,不就是文本文件吗?所以,把代码文本 echo
追加写入到目标文件里去即可:
targetpath="./${dir}/${filename}"
echo -e "/* Created by switch_generator */\n"\
> ${targetpath} # sleep 0.1
echo -e \
"int main(){\n\n\
int i = 0, j = 0;\n\
switch (i) {\
" >> ${targetpath}; # sleep 0.1
for (( i = 1;i <= $branch; i++ )); do
record=$i
i=$(( i*seperate ))
echo -e \
" case $i:\n\
j += $i;\n\
break;\n\
" >> ${targetpath}; # sleep 0.1
i=$record
done
echo -e \
" default:\n\
j += 1000;\n\
break;\n\
}\n\
return 0;\n\
}" >> ${targetpath}
源文件流水线
提升抽象的层次,用另一个脚本调用上面的脚本,实现源文件的批量生产,核心代码是:
for (( branch_num = 1; branch_num <= $size; branch_num++ ));do
filename="${compiler}_branch_${branch_num}"
bash ./switch_generator.sh -b $branch_num -d $dir -f ${filename}.c
# $compiler -S ./${dir}/${filename}.c -o ./${dir}/${filename}.s
# ...
done
把这一堆文件批量编译到汇编,再用 grep
检查是否存在跳转表并报告,我们的任务就完成了,道理差不多,这里不再赘述,如果你好奇讨论题的答案:
- 当连续分支数量 >= 4(clang) / 5(gcc) 时,编译器将使用跳转表,否则使用 subl, je 条件跳转;
- 当分支常量间隔 >= 12(clang) / 10(gcc) 时,编译器不再采用跳转表,而是直接用 subl, je 进行条件判断与跳转;
- 当分支变量为两段连续,但两段之间有较大间隔,如这里的 1,2,…,6, 101,102,…106,gcc 将生成两张跳转表. (此结论来自我的队友 LYT 同学)