Week1Day6:自制简易 OJ【2023 安全创客实践训练|笔记】

内容为武汉大学国家网络安全学院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.carray.h。当我们在命令行中执行make array.o时,根据这一规则,如果array.o不存在或者array.carray.h至少之一比array.o更新,就会执行gcc -c -o array.o array.c

我们把上述代码保存为Makefile,与array.carray.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.omake array.omake main。当我们执行make main时,make命令发现array.omain.o不存在,就会根据以它们为目标的规则先生成它们。

很多时候,你会需要将.o为后缀的目标代码文件和可执行的程序文件删除,完全从头进行编译。那么我们可以写一条clean规则,例如:

clean:
    rm -f array.o main.o main

rm命令表示删除文件,-f表示强制,因此rm -f array.o main.o main

按照预期,当我们执行make clean就可以删除array.omain.omain了。事实真的这样吗?

细心的同学已经发现,这时如果已经存在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;
}
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值