恢复 - CS50x 2026

待解决的问题
为了准备这个问题,我们在过去几天里在校园各处拍摄了一些照片,这些照片都以 JPEG 格式保存在数码相机的存储卡中。不幸的是,我们不知何故把它们全部删除了!谢天谢地,在计算机世界里,“删除”往往并不意味着真正的“删除”,而更多是“被遗忘”。尽管相机坚持认为存储卡现在是空的,但我们确信事实并非如此。实际上,我们希望(或者说,期待!)你能编写一个程序来帮我们恢复这些照片!
在 recover 文件夹中的 recover.c 文件中,编写一个程序从存储卡中恢复 JPEG。
分发代码
在这个问题中,你将扩展 CS50 团队为你提供的代码功能。
下载分发代码
登录 cs50.dev,点击终端窗口,然后单独执行 cd 命令。你应该会发现终端窗口的提示符类似于:
$
接着执行
wget https://cdn.cs50.net/2026/x/psets/4/recover.zip
以便将名为 recover.zip 的压缩文件下载到你的 Codespace。
然后执行
unzip recover.zip
来创建一个名为 recover 的文件夹。你不再需要这个 ZIP 文件,所以可以执行
rm recover.zip
并在提示符后输入 “y” 按回车,以删除你下载的 ZIP 文件。
现在输入
cd recover
接着按回车键进入(即打开)该目录。你的提示符现在应该类似于:
recover/ $
单独执行 ls,你应该会看到两个文件:recover.c 和 card.raw。
背景知识
尽管 JPEG 比 BMP 更复杂,但 JPEG 具有“签名”(signatures),即可以将它们与其他文件格式区分开来的字节模式。具体来说,JPEG 的前三个字节是:
0xff 0xd8 0xff
从第一个字节到第三个字节,从左到右。与此同时,第四个字节是 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 或 0xef 中的一个。换句话说,第四个字节的前四个位是 1110。
很有可能,如果你在已知存储照片的介质(例如,我的存储卡)上发现这四个字节的模式,它们就标志着一个 JPEG 的开始。公平地说,你也可能纯粹由于巧合而在某些磁盘上遇到这些模式,所以数据恢复并不是一门精确的科学。
幸运的是,数码相机倾向于在存储卡上连续存储照片,即每张照片都紧跟在之前拍摄的照片之后存储。因此,一个 JPEG 的开始通常标志着另一个 JPEG 的结束。然而,数码相机通常使用 FAT 文件系统初始化卡,其“块大小”为 512 字节 (B)。这意味着这些相机仅以 512 B 为单位写入存储卡。因此,一张 1 MB(即 1,048,576 B)的照片会在存储卡上占用 1048576 ÷ 512 = 2048 个“块”。但是,一张哪怕小一个字节的照片(即 1,048,575 B)也是如此!磁盘上浪费的空间被称为“松弛空间”(slack space)。取证调查人员经常在松弛空间中寻找可疑数据的残余。
所有这些细节意味着你,作为调查员,可以编写一个程序遍历我的存储卡副本,寻找 JPEG 的签名。每次找到签名时,你都可以打开一个新文件进行写入,并开始用我存储卡中的字节填充该文件,直到遇到另一个签名时才关闭该文件。此外,为了提高效率,你可以一次读取 512 个字节到一个缓冲区中,而不是一次读取一个字节。多亏了 FAT,你可以确信 JPEG 的签名将是“块对齐”的。也就是说,你只需要在一个块的前四个字节中寻找这些签名。
当然,请意识到 JPEG 可以跨越连续的块。否则,没有 JPEG 能大于 512 B。但是 JPEG 的最后一个字节可能不会恰好落在块的末尾。回想一下松弛空间的可能性。但不用担心。因为这张存储卡在我开始拍照时是全新的,所以它很可能已经被制造商“置零”(即填充了 0),在这种情况下,任何松弛空间都将填充 0。如果你恢复的 JPEG 结尾包含这些拖尾的 0 也没关系;它们仍然应该是可查看的。
现在,我只有一张存储卡,但你们有这么多人!所以我已经创建了这张卡的“取证映像”,将其内容逐字节存储在一个名为 card.raw 的文件中。为了让你不浪费时间在数百万个 0 上进行不必要的迭代,我只对存储卡的前几兆字节进行了成像。但你最终应该发现该映像包含 50 个 JPEG。
实现要求
实现一个名为 recover 的程序,从取证映像中恢复 JPEG。
- 在
recover目录下的recover.c文件中实现你的程序。 - 你的程序应该接受且仅接受一个命令行参数,即要从中恢复 JPEG 的取证映像的名称。
- 如果你的程序在执行时没有提供恰好一个命令行参数,它应该提醒用户正确的用法,并且
main函数应返回1。 - 如果取证映像无法打开进行读取,你的程序应该告知用户,并且
main函数应返回1。 - 你生成的每个文件都应命名为
###.jpg,其中###是一个三位十进制数,从第一个图像的000开始依次向上计数。 - 你的程序如果使用了
malloc,则不得发生任何内存泄漏。
提示
在编写更多代码之前先写一些伪代码
如果不确定如何解决较大的问题,请将其分解为可以先解决的小问题。例如,这个问题其实只是几个小问题:
接受一个命令行参数:存储卡的名称
打开存储卡
当存储卡中仍有数据可读时
- 从数据中创建 JPEG
让我们写一些伪代码作为注释来提醒你这样做:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 接受一个命令行参数
// 打开存储卡
// 当存储卡中仍有数据可读时
// 从数据中创建 JPEG
}
将伪代码转换为代码
首先,考虑如何接受一个命令行参数。如果用户错误使用了程序,你应该告诉他们程序的正确用法。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 接受一个命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// 打开存储卡
// 当存储卡中仍有数据可读时
// 从数据中创建 JPEG
}
既然你已经检查了正确的用法,你就可以打开存储卡了。请记住,你可以使用 fopen 以编程方式打开 card.raw,如下所示。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 接受一个命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// 打开存储卡
FILE *card = fopen(argv[1], "r");
// 当存储卡中仍有数据可读时
// 从数据中创建 JPEG
}
当然,你应该检查以确保文件已正确打开!如果没有打开,请告知用户并退出程序:我们将把这部分留给你。
接下来,你的程序应该从你打开的存储卡中读取数据,直到不再有数据可读为止。在此过程中,你的程序应该从 card.raw 中恢复每一个 JPEG,并将每一个作为单独的文件存储在当前工作目录中。
首先考虑如何通读 card.raw。回想一下,要从文件中读取数据,你需要将该数据临时存储在“缓冲区”中。并且进一步回想一下, card.raw 以 512 字节的块存储数据。因此,你可能会想要创建一个 512 字节的缓冲区来存储顺序读取的数据块。一种方法是使用 stdint.h 中的 uint8_t 类型,它恰好存储 8 位(1 字节)。该类型之所以称为 uint8_t,是因为它存储一个需要 8 位空间(即一个字节)的无符号/正数/非负整数。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 接受一个命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// 打开存储卡
FILE *card = fopen(argv[1], "r");
// 为一个数据块创建缓冲区
uint8_t buffer[512];
// 当存储卡中仍有数据可读时
// 从数据中创建 JPEG
}
不过,在这里将 512 用作“魔数”可能不是最好的主意。很有可能你可以进一步改进这个设计!
现在,考虑如何从存储卡中读取数据。根据其手册页,fread 返回它已读取的字节数,在这种情况下,考虑到 card.raw 包含若干 512 字节的块,它应该返回 512 或 0。为了从 card.raw 读取每一个块,在使用 fopen 打开它之后,使用如下循环应该就足够了。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 接受一个命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// 打开存储卡
FILE *card = fopen(argv[1], "r");
// 为一个数据块创建缓冲区
uint8_t buffer[512];
// 当存储卡中仍有数据可读时
while (fread(buffer, 1, 512, card) == 512)
{
// 从数据中创建 JPEG
}
}
这样,一旦 fread 返回 0(实际上就是 false),你的循环就会结束。
最后,在你继续阅读 card.raw 的过程中,如何以编程方式创建 JPEG 取决于你。为此,你可能会发现下面的视频演示对你很有用。
请记住,你的程序应该通过将每个文件命名为 ###.jpg 来对输出的文件进行编号,其中 ### 是从 000 开始的三位十进制数。熟练使用 sprintf,并注意 sprintf 将格式化的字符串存储在内存中的某个位置。考虑到 JPEG 文件名规定的 ###.jpg 格式,你应该为该字符串分配多少字节?(不要忘记 NUL 字符!)
要检查你的程序吐出的 JPEG 是否正确,只需双击并查看即可!如果每张照片看起来都是完整的,那么你的操作可能就成功了!
当然,记得 fclose 每一个你用 fopen 打开的文件!
保持工作目录整洁
你编写的第一版代码吐出的 JPEG 很有可能是不正确的。(如果你打开它们却看不到任何东西,它们可能就不正确!)执行下面的命令删除当前工作目录中的所有 JPEG。
rm *.jpg
如果你不希望在每次删除时都收到确认提示,请执行以下命令。
rm -f *.jpg
只是使用 -f 开关时要小心,因为它会“强制”删除而不会提示你。
视频演示
如何测试
运行程序
./recover card.raw
正确性
check50 cs50/problems/2026/x/recover
风格
style50 recover.c
如何提交
在你的终端中,执行以下命令来提交你的作品,并按提示进行操作。
submit50 cs50/problems/2026/x/recover