内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将与15个工作日内将博客设置为仅粉丝可见。
Makefile
在前面学习多模块程序的时候,我们需要先把每个模块的代码都生成为目标代码文件,然后再将目标代码文件联编成一个可执行文件。如果每一次编译都要输入这么多命令,是不是很复杂呢?如果每次修改一点点内容就需要重新编译整个工程,是不是很浪费时间呢?
为了解决所遇到的问题,方便开发,我们使用一个叫做make
的命令,它可以读取Makefile
文件,并且根据Makefile
中的规则描述把源文件生成为可执行的程序文件。
最基本的Makefile
中包含了一系列形式如下的规则。请注意,每一条规则的命令前,必须要有一个制表符\t
。
目标: 依赖1 依赖2 ...
命令
例如,我们可以写一条规则:
array.o: array.c array.h
gcc -c -o array.o array.c
表示生成的文件是目标代码文件array.o
,它依赖于array.c
和array.h
。当我们在命令行中执行make array.o
时,根据这一规则,如果array.o
不存在或者array.c
与array.h
至少之一比array.o
更新,就会执行gcc -c -o array.o array.c
。
我们把上述代码保存为Makefile
,与array.c
和array.h
放在同一目录,在那个目录里执行make array.o
就能看到效果。注意:Makefile
里的除当前目录隐藏文件外的第一个目标会成为运行make
不指定目标时的默认目标。
main: array.o main.o
gcc -o main array.o main.o
main.o: main.c array.h
gcc -c -o main.o main.c
array.o: array.c array.h
gcc -c -o array.o array.c
在Makefile
有多条规则时,如果我们希望只生成其中一个,我们可以在make
命令后加上需要生成的目标的名称。例如,在这里我们可以执行make main.o
、make array.o
或make main
。当我们执行make main
时,make
命令发现array.o
和main.o
不存在,就会根据以它们为目标的规则先生成它们。
很多时候,你会需要将.o
为后缀的目标代码文件和可执行的程序文件删除,完全从头进行编译。那么我们可以写一条clean
规则,例如:
clean:
rm -f array.o main.o main
rm
命令表示删除文件,-f
表示强制,因此rm -f array.o main.o main
按照预期,当我们执行make clean
就可以删除array.o
、main.o
和main
了。事实真的这样吗?
细心的同学已经发现,这时如果已经存在clean
文件,rm
命令就不会执行了。为了解决这个问题,我们通过一个特殊的方法告诉make
这个名为clean
的规则在clean
存在的时候仍然有效。
.PHONY: clean
clean:
rm -f array.o main.o main
.PHONY
用于声明一些伪目标,伪目标与普通的目标的主要区别是伪目标不会被检查是否存在于文件系统中而默认不存在且不会应用默认规则生成它。
在Makefile
中我们还可以使用它的变量和注释。
# 井号开头的行是一个注释
# 设置 C 语言的编译器
CC = gcc
# -g 增加调试信息
# -Wall 打开大部分警告信息
CFLAGS = -g -Wall
# 整理一下 main 依赖哪些目标文件
MAINOBJS = main.o array.o
.PHONY: clean
main: $(MAINOBJS)
$(CC) $(CFLAGS) -o main $(MAINOBJS)
array.o: array.c array.h
$(CC) $(CFLAGS) -c -o array.o array.c
main.o: main.c array.h
$(CC) $(CFLAGS) -c -o main.o main.c
clean:
rm -f $(MAINOBJS) main
上面这个例子已经是一个较为完整的Makefile
了。以#
开头的是我们的注释,我们在这里用注释说明了我们定义的Makefile
变量的用途。CC
变量定义了编译器,CFLAGS
变量标记了编译参数,MAINOBJS
变量记录了main
依赖的目标文件。定义的变量可以直接通过$(变量名)
进行使用。
自制简易 OJ
研究真理可以有三个目的:当我们探索时,就要发现到真理;当我们找到时,就要证明真理;当我们审查时,就要把它同谬误区别开来。——帕斯卡(法国)
在本节中我们首先要探索一个错误程序的错误原因并予以修正,但不要求对其正确性进行数学证明,然后要编写一个 OJ 核心,审查其正确性。
完整目标
首先用gdb
调试并修正一个关于判断三角形的程序。然后编写一个简单的 OJ 核心判断这个程序或者其它一个仅操作于标准输入输出的程序是否正确。
给出的结构
Makefile
可以运行make
进行自测。src
编写和编译代码的目录,本节要求的所有任务都在这里完成。本节中,在终端里我们可以输入pushd src
并按回车进入这个目录,之后可以输入popd
并按回车退回进入之前的目录。Makefile
给出的正确的用于编译 OJ 的 Makefile。angle.c
这是需要被调试的错误地判断三角形的程序代码。oj.c
在这里编写 OJ 的主程序。run.c
提供函数void run(const char *program, const char *file_in, const char *file_out)
,其中第一个参数表示要运行的程序的路径是program
,第二个参数表示把路径为file_in
的文件的内容作为program
的标准输入内容,第三个参数表示program
的标准输出会输出到路径为file_out
的文件。run.h
这是run.c
对应的头文件。
srctestbyscript
(若存在) 运行自测用的目录,不必阅读和修改其中内容。usertest.sh
与自测相关的文件,不必阅读和修改其中内容。
第一部分
先修正angle
的编译和链接错误。(提示:只是两处简单的笔误)通过gdb
来调试,把angle.c
修正为下述题目的正确代码:
-
判断三角形
输入三个小于一万的正整数表示三角形的三个边长。
若不存在这样的三角形,输出
It's not a triangle
若存在这样一个锐角三角形,输出
It's an acute triangle
若存在这样一个钝角三角形,输出
It's an obtuse triangle
若存在这样一个直角三角形,输出
It's a right triangle
-
样例输入
5 4 3
-
样例输出
It's a right triangle
第二部分
测试./program
输入为in.txt
,输出到out.txt
,正确答案是right.txt
的情况下的正确性。如果正确,输出Accept
,否则输出Wrong Answer
。(只需要测试一次)
注意:一个程序是正确的当且仅当它的输出与标准输出完全一致或它的输出比标准输出的结尾多或少一个换行符('\n'
)。
Project
由上至下依次为
.PHONY: run
run:
bash usertest.sh
CC = gcc
CFLAGS = -std=c99 -g -Wall -Wextra
OBJS = oj.o run.o
.PHONY: clean
oj: $(OBJS)
oj.o: run.h
run.o: run.h
clean:
$(RM) -- $(OBJS) oj
#incldue <stdio.h>
void swap(int *p, int *q)
{
int *temp;
temp = p;
p = q;
q = temp;
}
int mian(void)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > c) {
swap(&a, &c);
}
if (b > c) {
swap(&b, &c);
}
if (a + b <= c) {
puts("It's not a triangle");
} else if (a * a + b * b < c * c) {
puts("It's an obtuse triangle");
} else if (a * a + b * b > c * c) {
puts("It's an acute triangle");
} else {
puts("It's a right triangle");
}
return 0;
}
#include <stdio.h>
#include "run.h"
int main()
{
const char *name_program = "./program";
const char *name_in = "./in.txt";
const char *name_out = "./out.txt";
const char *name_right = "./right.txt";
run(name_program, name_in, name_out);
FILE *fpright = fopen(name_right, "r");
FILE *fpout = fopen(name_out, "r");
// Input your code here.
fclose(fpout);
fclose(fpright);
return 0;
}
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "run.h"
void run(const char *program, const char *filein, const char *fileout)
{
int pid = fork();
if (pid < 0) {
return;
} else if (pid == 0) {
int fdin = open(filein, O_RDONLY);
dup2(fdin, 0);
close(fdin);
int fdout = open(fileout, O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fdout, 1);
close(fdout);
execlp(program, program, (char *)0);
exit(1);
} else {
waitpid(pid, NULL, 0);
}
}
#ifndef __RUN_H__
#define __RUN_H__ 1
void run(const char *program, const char *filein, const char *fileout);
#endif
if [ ! -d srctestbyscript ]; then
mkdir srctestbyscript
fi
cp -- src/* srctestbyscript
cd srctestbyscript
if ! make angle > /dev/null; then
echo "angle 编译失败"
else
echo "angle 编译成功"
echo 三角形自测
RA=0
RAT=1
if echo "65 72 97" | ./angle | grep "right" > /dev/null; then
echo "输入 65 72 97 输出正确"
let RA=RA+1
else
echo "输入 65 72 97 输出错误"
fi
if echo "55 73 48" | ./angle | grep "right" > /dev/null; then
echo "输入 55 73 48 输出正确"
let RA=RA+1
else
echo "输入 55 73 48 输出错误"
fi
if echo "5 4 3" | ./angle | grep "right" > /dev/null; then
echo "输入 5 4 3 输出正确"
else
echo "输入 5 4 3 输出错误"
RAT=0
fi
echo "5 4 3" | ./angle | grep "obtuse" > /dev/null && echo "输入 5 4 3 输出错误" && let RAT=0
echo "5 4 3" | ./angle | grep "acute" > /dev/null && echo "输入 5 4 3 输出错误" && let RAT=0
let RA=RA+RAT
echo 三角形自测完毕
echo 自测 oj.c
echo "5 4 3" > in.txt
./angle < in.txt > out.txt
cp out.txt right.txt > /dev/null
mv angle program > /dev/null
if ! make oj > /dev/null; then
echo "oj.c 编译失败"
else
echo "oj.c 编译成功"
R=0
if ./oj | grep "Accept" > /dev/null; then
let R=R+1
else
echo "错误"
fi
echo "" >> right.txt
if ./oj | grep "Accept" > /dev/null; then
let R=R+1
else
echo "处理换行错误"
fi
echo "" >> right.txt
if ./oj | grep "Wrong" > /dev/null; then
let R=R+1
else
echo "处理多个换行错误"
fi
fi
echo "oj.c 自测完毕"
echo "angle 测试共 3 组,正确 $RA 组。"
echo "oj 测试共 3 组,正确 $R 组。"
fi
我的答案
#include <stdio.h>
void swap(int *p, int *q)
{
int temp;
temp = *p;
*p = *q;
*q = temp;
}
int main(void)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > c) {
swap(&a, &c);
}
if (b > c) {
swap(&b, &c);
}
if (a + b <= c) {
puts("It's not a triangle");
} else if (a * a + b * b < c * c) {
puts("It's an obtuse triangle");
} else if (a * a + b * b > c * c) {
puts("It's an acute triangle");
} else {
puts("It's a right triangle");
}
return 0;
}
#include <stdio.h>
#include "run.h"
int main()
{
const char *name_program = "./program";
const char *name_in = "./in.txt";
const char *name_out = "./out.txt";
const char *name_right = "./right.txt";
run(name_program, name_in, name_out);
FILE *fpright = fopen(name_right, "r");
FILE *fpout = fopen(name_out, "r");
fseek(fpright,0,SEEK_END);
fseek(fpout,0,SEEK_END);
int len1 = ftell(fpright);
int len2 = ftell(fpout);
if (abs(len1 - len2) > 1) {
printf("Wrong Answer");
return 0;
}
fseek(fpright,0,SEEK_SET);
fseek(fpout,0,SEEK_SET);
char right_c,out_c;
int right_val,out_val;
while(1) {
right_val = fread(&right_c, 1, 1, fpright);
out_val = fread(&out_c, 1, 1, fpout);
if (!right_val || !out_val) {
break;
}
if (right_c != out_c) {
printf("Wrong Answer");
return 0;
}
}
if (right_val && right_c != '\n') {
printf("Wrong Answer");
return 0;
}
if (out_val && out_c != '\n') {
printf("Wrong Answer");
return 0;
}
printf("Accept");
fclose(fpout);
fclose(fpright);
return 0;
}