简介:《Games101-作业0:虚拟机的使用》PDF面向计算机图形学入门学习者与高校课程学生,尤其是初次接触GAMES101课程、需要搭建统一实验环境的人群。文档围绕虚拟机环境的完整搭建流程展开,涵盖VirtualBox安装、虚拟硬盘下载与挂载、Guest Additions增强功能配置,以及作业框架的传输与编辑方式;同时简单介绍开发工具选择与C++常见报错排查思路,如编译错误、链接错误、段错误等。资源包共1个PDF文件,大小约874KB,内容精炼、查阅方便,适合边操作边对照。目前已有435人学习下载。读者可借助这份材料快速完成从环境配置到代码编译运行的入门闭环,减少因系统差异带来的配置障碍,为后续完成图形学作业打下基础。
1. 虚拟机装完发现拖不进文件:作业 0 真正卡人的不是图形学
GAMES101 作业 0 名字叫「虚拟机的使用」,很多人以为半小时能收工,结果一个晚上全耗在 VirtualBox 新建向导、增强功能装不上、文件拖不进虚拟机、cmake ..找不到 Eigen 这几处。这类故障最难受的地方在于报错信息都很含糊:要么是英文界面里一句Failed,要么干脆什么都不提示,虚拟机就在那儿黑屏或者停在登录页。下面按实际翻车顺序拆开讲——虚拟机建不起来、Guest Additions 安装失败、链接期undefined reference、运行期段错误,最后落到怎么用一条报错文本反推卡在哪一层。刚上手 C++ 和图形学的同学照着命令能走通,写过几年代码的人也能看到每一种报错背后的真实原因。
2. 虚拟机建不起来:新建向导、vdi 挂载与首次启动的三种报错
2.1 新建时直接报 VT-x 不可用:类型、版本、内存三处默认值
新建向导第一步就有两个容易选错的项:类型选Linux,版本选Ubuntu (64-bit)。虚拟机文件是 64 位的 Ubuntu 18.04,版本选成 32 位的话,开机瞬间会抛This kernel requires an x86-64 CPU, but only detected an i686 CPU。这个报错看起来像内核问题,实际上跟镜像一点关系都没有,纯粹是新建时那个下拉框选错了。已经建好的虚拟机改不回来,只能删掉重建,所以这一步值得停三秒确认。
内存建议 2GB,这是文档里给的值。给 512MB 会出现另一种症状:开机进图形界面时黑屏、卡在紫色背景,或者登录后桌面加载一半没了。这不是虚拟机坏了,是内存不够让桌面环境被 OOM 杀掉。主机只有 8GB 内存的话,2GB 是个比较稳的平衡点,别为了流畅度往 4GB 上加,主机自己会开始换页。
真正让人摸不着头脑的是第三种:点启动,VirtualBox 弹窗报VT-x is not available (VERR_VMX_NO_VMX)或者VT-x is disabled in the BIOS for all CPU modes。这条跟 GAMES101 无关,是宿主机的问题,两条路径分别处理:
# 第一步:确认宿主机 CPU 是否支持并开启了硬件虚拟化 # Windows 在任务管理器 -> 性能 -> CPU 右下角看「虚拟化:已启用」 # Linux 下直接查 CPU 标志位,有 vmx(Intel)或 svm(AMD)即为支持 grep -E -c '(vmx|svm)' /proc/cpuinfo返回值大于 0 说明 CPU 支持,那问题就在软件层。Windows 上最常见的原因是系统自带的 Hyper-V 或「虚拟机监控程序平台」占着虚拟化指令,VirtualBox 抢不到。解决方式是进「启用或关闭 Windows 功能」,把 Hyper-V、Windows 虚拟机监控程序平台、虚拟机平台这三项取消勾选,重启后再开 VirtualBox。返回值等于 0 就得进 BIOS 开虚拟化,一般在 CPU Configuration 或 Security 页里,名字叫 Intel Virtualization Technology 或 SVM Mode。
提示:改完 BIOS 或系统功能后必须完全关机再开机,重启(Restart)在某些主板上不会重新初始化虚拟化状态。
2.2 虚拟硬盘挂不上:解压顺序和「使用已有的虚拟硬盘文件」
下载下来的是一个.rar压缩包,解压后才是.vdi虚拟硬盘文件。VirtualBox 不认识 rar,直接在选择硬盘的对话框里指过去,文件列表里根本看不到它,很多人到这一步以为是文件损坏。先解压,再新建。
新建向导走到「硬盘」这一步,要选的是「使用已有的虚拟硬盘文件」,然后点右侧那个小文件夹图标,通过「添加」把解压出来的.vdi注册进来。如果手快点了「现在创建虚拟硬盘」,也不用删虚拟机重来,在虚拟机关机状态下进「设置 → 存储」,选中控制器下面的 SATA 控制器,点「添加硬盘」补挂即可。
这一步有个高频报错:
Cannot register the hard disk '...vdi' with UUID {xxxxxxxx-xxxx-...} because a hard disk '...vdi' with UUID {xxxxxxxx-xxxx-...} already exists.原因是你之前建过一次虚拟机、后来把虚拟机删了,但虚拟硬盘还挂在 VirtualBox 的媒体注册表里。UUID 是写死在 vdi 文件头里的,复制一份文件也不能绕过。处理方式是先把旧记录清掉再重新挂:
# 列出 VirtualBox 当前注册的所有虚拟硬盘,找到那条重复的 UUID VBoxManage list hdds # 从注册表移除该硬盘记录(--delete 会连文件一起删,谨慎使用) # 只想解除注册、保留文件,用下面这条 VBoxManage closemedium disk <上一步查到的UUID> # 如果确认旧文件已经没用了,连文件一起清掉 VBoxManage closemedium disk <UUID> --deleteclosemedium的第二个参数是 UUID 而不是文件路径,因为同一个路径可能注册了多次,按 UUID 定位最准。--delete是真删文件,第一次操作建议不加,先解除注册、确认新虚拟机能用再说。
还有一种NS_ERROR_FAILURE (0x80004005)打开硬盘失败,八成是 vdi 放在了带中文或者带空格的路径下,或者解压时磁盘满了导致文件被截断。把 vdi 挪到一个纯英文短路径,比如D:\vm\下面,重新挂一次通常就好了。虚拟硬盘动辄几个 GB,路径越短越省事。
2.3 停在登录界面:密码输对了还是进不去
系统密码在文档里写得很清楚,首字母大写。但 Ubuntu 登录界面有两个设计会让新手怀疑人生:输入框不显示任何字符,连星号都没有;输错一次要等几秒才能重试。于是「我明明输了但屏幕没反应」和「密码错了」看起来一模一样。
先确认键盘布局。如果宿主机是中文输入法状态,进虚拟机后按下的键可能被吞掉或者映射错误,先切到英文输入再输密码。其次确认大小写,Ilovegraphics里第一个字母是大写 I,不是小写 l,也不是数字 1,这三个字符在某些字体下几乎一样。
如果密码确认无误还是进不了桌面,退回 2.1 那条:内存给少了,登录验证其实已经通过,是桌面环境启动到一半被杀。可以在登录界面按Ctrl+Alt+F3切到字符终端,用同样的账号密码登录,能进去就说明系统没问题,是图形层资源不足。从字符终端里执行sudo shutdown -h now关机,回 VirtualBox 设置里把内存调到 2GB 再启动。
3. 增强功能装不上:Guest Additions 失败的终端补救和拖放失效
3.1 点「安装增强功能」没反应:先看光驱有没有挂上
虚拟机跑起来以后,菜单栏「设备 → 安装增强功能」这个动作本质上是把 VirtualBox 自带的一个 ISO 镜像塞进虚拟光驱。点完之后如果桌面没有任何反应,不要急着怀疑系统,先在虚拟机里开终端看光驱状态:
# 查看块设备,sr0 一般就是虚拟光驱 lsblk # /dev/cdrom 是一个指向实际光驱设备的软链接,确认它指向哪里 ls -l /dev/cdrom # 如果系统没有自动挂载,手动挂一下看有没有内容 sudo mount -t auto /dev/cdrom /media/cdrom ls /media/cdromlsblk里sr0那一行如果有rom标记、大小几百 MB,说明 ISO 挂上了,问题出在自动挂载没触发,手动挂载即可。如果sr0压根不存在,说明菜单那一下没生效,常见于虚拟机窗口没获得焦点、或者菜单点在了「设备」的其他项上。把虚拟机窗口点一下再重新点一次菜单。
还有一类是光驱被占着。之前挂载过一次没卸载,再点安装就会静默失败。先sudo umount /media/cdrom卸掉,再重新点菜单。
3.2 图形界面失败后的终端安装流程
图形界面那条路走不通时,文档给的终端方案是有效的,但四条命令每一条都可能单独失败,逐条解释一下:
# 1. 建挂载点。-p 的作用是父目录已存在时不报错,重复执行安全 sudo mkdir -p /media/cdrom # 2. 挂载光驱。-t auto 让内核自己识别文件系统类型,ISO 会被识别成 iso9660 sudo mount -t auto /dev/cdrom /media/cdrom # 3. 进入挂载点,安装脚本就在这一层 cd /media/cdrom # 4. 执行安装脚本。必须用 sudo,脚本要往 /usr 下写文件、编译内核模块 sudo sh VBoxLinuxAdditions.run第 2 步最容易出问题,典型报错是mount: /dev/sr0: can't read superblock。这几乎总是因为光驱里根本没盘——你没点菜单,或者点了但 VirtualBox 没来得及挂载。回到 3.1 用lsblk确认。另一种是mount point does not exist,那是第 1 步没执行或者被中断了,重新跑一遍。
第 4 步的VBoxLinuxAdditions.run是个自解压脚本,跑起来后会往终端打一堆编译日志,滚动很快。如果中途报This system is not currently set up to build kernel modules,说明缺内核头文件和编译工具链,增强功能里的共享文件夹、拖放这些依赖内核模块的功能都装不上。常见做法是先补齐依赖再重跑:
# 安装编译工具和与当前内核版本严格对应的头文件 sudo apt-get update sudo apt-get install -y build-essential linux-headers-$(uname -r) # 依赖装好后重新执行安装脚本 cd /media/cdrom && sudo sh VBoxLinuxAdditions.run$(uname -r)会展开成当前运行的内核版本号,头文件版本必须和它完全一致,装成别的版本一样编译不过。这一步的前提是虚拟机能联网,如果网络不通,就得从宿主机把对应的 deb 包拖进去手动装,那就得先解决拖放问题——所以顺序上建议先确认网络,再折腾增强功能。
脚本跑完不会自动生效,必须重启:
sudo reboot重启后验证是否真的装上了,看有没有VBoxService进程:
ps aux | grep -i vbox能看到VBoxService和VBoxClient相关的进程就说明装好了。一个都没有就是没装成功,回上面重来。
3.3 拖放设成双向仍然拖不进去的三个原因
「设备 → 拖放 → 双向」这个设置只对当前运行的虚拟机会话生效,而且它依赖增强功能里的宿主服务。三个排查点按顺序过:
第一,增强功能是不是真装上了。用 3.2 结尾那条ps aux验证,没进程就是没装,设置成双向也没用。
第二,设置完之后有没有重启虚拟机。拖放服务的加载在开机阶段,改设置不重启,服务不会重新读配置。
第三,桌面环境本身对拖放的支持不完整。Ubuntu 18.04 默认的 GNOME 在 Wayland 会话下拖放行为经常异常,登录界面右下角那个齿轮可以切到 Xorg 会话再试。我一般会直接绕开拖放,用共享文件夹,稳定性高得多:
# 1. 先把当前用户加入 vboxsf 组,否则挂载点没有读权限 sudo usermod -aG vboxsf $USER # 2. 这一步之后必须注销重新登录(或重启),组权限才会生效 # 3. 在 VirtualBox 菜单「设备 -> 共享文件夹」里添加一个主机目录,勾选自动挂载 # 4. 重新登录后确认挂载结果 ls /media/sf_<共享文件夹名>usermod -aG里的-a是关键,漏掉它会把用户从其他附加组里踢出去,导致 sudo 都用不了。共享文件夹在虚拟机里的挂载点固定是/media/sf_加上你在设置里填的名字,勾了「自动挂载」每次开机都会挂上,比拖放可靠。
4. 编译与运行报错:cmake .. 通过不了、undefined reference 和段错误
4.1 cmake .. 报错:build 目录和 CMakeLists.txt 的位置关系
文档给的编译流程是四步,但每步都有人在同一个地方翻车:
# 在 main.cpp 所在的目录下执行 mkdir build # 建一个独立的构建目录,避免编译产物污染源码目录 cd build # 进入构建目录 cmake .. # .. 表示上一级目录,CMake 会去那里找 CMakeLists.txt make # 按生成的规则编译,错误直接打在终端 ./Transformation # 运行产物,名字来自 CMakeLists.txt 里的目标名cmake ..报The source directory "..." does not appear to contain CMakeLists.txt,只有两种可能:你在错误的层级执行了命令,或者CMakeLists.txt根本没跟着框架一起拷进虚拟机。后者特别常见——从宿主机拖文件时只拖了main.cpp,把CMakeLists.txt落下了。先ls ..看看上一级目录里到底有什么。
另一个高频报错是cmake: command not found,虚拟机里没装 CMake。Ubuntu 18.04 自带源里就有:sudo apt-get install -y cmake。同理make: command not found装build-essential。
..和.的区别值得记一下:cmake ..是去上一级找源码,cmake .是在当前目录找。在 build 目录里写成cmake .会报找不到 CMakeLists.txt,因为 build 里本来就没有。
4.2 undefined reference to xxx:链接阶段的问题不在代码里
这类报错长得像编译错误,实际上编译已经过了,是链接器找不到符号定义。区分方法很简单:报错里带undefined reference to就是链接期,带error:加行号的是编译期。
最常见的三种成因,对应三种解法:
第一种,undefined reference to 'std::cout'这类标准库符号缺失。通常是源文件没#include <iostream>,或者CMakeLists.txt里没声明 C++ 标准,编译器按老标准走导致名字修饰对不上。在 CMake 里显式指定:
cmake_minimum_required(VERSION 3.10) project(Transformation) # 显式指定 C++ 标准,避免默认落到 C++98 导致标准库符号对不上 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Eigen 装在 /usr/include/eigen3,不在默认搜索路径里,必须加这一行 include_directories("/usr/include/eigen3") # 有几个 .cpp 就列几个,漏一个就会报 undefined reference add_executable(Transformation main.cpp)add_executable第一个参数是生成的可执行文件名,也就是最后./后面跟的那个名字。改了它,./Transformation就得跟着改。第二个参数往后是所有参与编译的源文件,后期作业拆成多个.cpp时,这里漏写一个,链接器就会报某个函数 undefined——因为那个函数的定义压根没被编进去。
第二种,用了需要额外链接的库,比如多线程的pthread_create,得显式告诉 CMake:
# 链接系统线程库,CMake 3.1 之后更推荐用 Threads 包,这里用最直观的写法 target_link_libraries(Transformation pthread)第三种,自己写的函数只在头文件里声明了,对应的.cpp里没实现,或者实现了但文件没进add_executable。这种情况报错里的符号名往往是你自己起的函数名,顺着名字找定义在哪就行。
注意:改完
CMakeLists.txt直接make有时不生效,CMake 的缓存会保留旧配置。保险做法是回 build 目录上层重新cmake ..,或者干脆rm -rf build从头来一遍。
4.3 运行期三类崩溃:Segmentation Fault、Bus Error、Math Error
程序编过了、能跑,但跑着跑着终端打出一行Segmentation fault (core dumped)就没了。这三类运行期错误的区分方式是:
Segmentation Fault是访问了不属于自己的内存。新手场景里最常见的不是野指针,而是在函数里定义了超大数组,比如double buffer[10000000],局部变量放在栈上,栈默认只有 8MB,一进去就爆。改成全局变量或者用new/std::vector放堆上就能解决。定位用 gdb 最快:
# 带调试信息重新编译,-g 会保留行号信息 g++ -g main.cpp -o Transformation # 用 gdb 跑,崩溃时 bt 打出调用栈,直接定位到出错行 gdb ./Transformation # 在 gdb 里输入 run,崩了之后输入 btBus Error成因和段错误接近,但多了一层硬件层面的对齐要求,比如把char*强转成int*再解引用,地址不是 4 的倍数就会触发。新手作业里少见,遇到时优先查指针类型转换。
Math Error基本只有一个来源:整数除零。int a = 1 / 0;直接崩。这里有个反直觉的点——浮点除零不会崩,1.0f / 0.0f得到的是inf,0.0f / 0.0f得到的是nan,程序照样往下跑,只是结果不对。所以如果你的程序没崩但算出来的坐标是nan,回头查有没有哪个分母在某种输入下变成了 0。
5. 避坑记录:五条「明明照着文档做了还是错」
5.1 现象:sin(30) 算出来不是 0.5,而是 -0.988
原因很直接:C++ 的std::sin吃的是弧度,不是角度。30 弧度换算成角度大约是 1718 度,落到单位圆上的位置跟 30 度毫无关系。文档里那段std::sin(30.0/180.0*acos(-1))就是在做角度转弧度,acos(-1)是 π,30.0/180.0是角度占半圆的比例,乘起来正好是 π/6。
这个坑在作业 0 里表现为观察题,在后面的作业里就是实打实的结果错误:变换矩阵里的 cos 和 sin 直接填角度值,算出来的坐标会离谱到看起来像公式推导错了,实际只是单位没换。我的习惯是统一写一个转换函数,不在各处手写:
#include <cmath> // 角度转弧度,所有涉及三角函数的地方都走这个函数,杜绝单位混用 inline float deg2rad(float deg) { return deg / 180.0f * std::acos(-1.0f); }acos(-1.0f)比硬编码3.14159精度高,也不用记 π 后面的位数。std::acos(-1)返回的是 double,和 float 混算会有隐式转换警告,加f后缀统一类型。
5.2 现象:fatal error: Eigen/Core: No such file or directory
原因:Eigen 通过系统包管理器安装后,头文件落在/usr/include/eigen3/Eigen/下面,而/usr/include/eigen3不在编译器的默认搜索路径里。所以#include <Eigen/Core>找不到,必须么写全路径#include <eigen3/Eigen/Core>,要么在 CMake 里把那个目录加进搜索路径。
两种做法选一种,不要混。我一般选 CMake 那种,代码里写<Eigen/Core>更干净,将来换平台也不用改源码:
# 把 eigen3 目录加进头文件搜索路径,源码里就能直接 #include <Eigen/Core> include_directories("/usr/include/eigen3")验证 Eigen 到底装在哪,用dpkg -L libeigen3-dev | grep Core查一下最准。如果这条命令什么都没输出,说明压根没装,先sudo apt-get install -y libeigen3-dev。
5.3 现象:./Transformation: No such file or directory
原因有两种。第一种是当前目录不对——可执行文件生成在 build 目录里,你如果cd ..回到源码目录再执行,当然找不到。ls看一眼当前目录里有没有这个文件,没有就cd build再来。
第二种是CMakeLists.txt里的目标名和你敲的命令不一致。add_executable(Transformation main.cpp)生成的是Transformation,如果那份框架里的目标名被改过,或者你从别处拷了一份 CMakeLists,产物名就变了。ls看 build 目录里实际生成了什么,按实际名字执行。
5.4 现象:程序能跑,但算出来的坐标跟手算对不上
这条最折磨人,因为不报错。作业 0 要求把点 (2,1) 先绕原点逆时针旋转 45 度、再平移 (1,2),用齐次坐标计算。手算结果是约 (1.707, 4.121),程序输出如果差得远,先查矩阵乘法顺序。
Eigen 里矩阵乘法是从右往左作用的。trans * rot * p表示先对 p 做 rot,再做 trans;写成rot * trans * p就变成了先平移再旋转,结果完全不同。这不是 Eigen 的坑,是线性代数里变换复合的书写习惯,但写代码时特别容易顺手写反。
另一个隐形的坑是齐次坐标的第三分量。二维点写成Vector3f(x, y, 1),那个 1 不能省也不能填别的,平移量就藏在这个维度里。如果第三分量填了 0,平移会完全不生效,结果只旋转不平移,看起来像平移矩阵写错了。
#include <iostream> #include <cmath> #include <eigen3/Eigen/Core> int main() { // 齐次坐标,第三分量必须是 1,否则平移失效 Eigen::Vector3f p(2.0f, 1.0f, 1.0f); float theta = 45.0f / 180.0f * std::acos(-1.0f); // 旋转矩阵,右上角是 0,平移量在第三列 Eigen::Matrix3f rot; rot << std::cos(theta), -std::sin(theta), 0.0f, std::sin(theta), std::cos(theta), 0.0f, 0.0f, 0.0f, 1.0f; // 平移矩阵,旋转部分为单位阵,第三列放 (1, 2) Eigen::Matrix3f trans; trans << 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f; // 左乘顺序即作用顺序:先 rot 后 trans Eigen::Vector3f result = trans * rot * p; std::cout << result.transpose() << std::endl; // 期望约 1.70711 4.12132 1 return 0; }rot的第三行第三列是 1,保证齐次坐标的第三分量乘完还是 1;trans的平移量放在第三列而不是第三行,因为 Eigen 的<<是按行填充的,第一行三个数对应矩阵第一行。这两处填错都不会报错,只会让结果偏掉。
5.5 现象:第二天开机虚拟机起不来,提示找不到启动介质
原因基本是非正常关机。直接点虚拟机窗口的关闭叉、或者在系统还在写盘时强制断电,vdi 文件可能处于不一致状态,下次启动 VirtualBox 会报FATAL: Could not read from the boot medium或者卡在 grub 提示符。
正确的关机方式是在虚拟机里执行sudo shutdown -h now,或者菜单「设备 → 正常关机」等它自己走完流程。已经出问题了的话,先检查 VirtualBox 的「设置 → 存储」里虚拟硬盘还在不在,有时候只是控制器配置丢了,重新挂一次 vdi 就能恢复。真要救数据,把 vdi 挂到另一台正常的虚拟机上做从盘读出来,比在原机上反复重启靠谱。
提示:做作业期间养成开快照的习惯,VirtualBox 菜单「机器 → 拍摄快照」,出问题几秒钟就能回滚,比修 vdi 省事得多。
6. 快速定位:从报错文本反推卡在哪一层
排这类环境问题,效率最高的方式不是逐个试,而是先给报错分层。虚拟机的故障基本落在这四层里,每层的判断依据和第一条命令是固定的:
| 报错特征 | 所在层级 | 第一条命令 | 判断依据 |
|---|---|---|---|
VERR_VMX_*、x86-64 CPU | 宿主机与虚拟化 | grep -cE '(vmx|svm)' /proc/cpuinfo | 返回 0 是 BIOS 或系统功能没开 |
Cannot register the hard disk、0x80004005 | 存储层 | VBoxManage list hdds | 看 UUID 是否重复、路径是否含中文 |
mount: can't read superblock、run 脚本报错 | 客户机挂载与内核模块 | lsblk、ps aux | grep -i vbox | sr0 是否存在、VBoxService 是否在跑 |
undefined reference、Segmentation fault | 编译与运行 | ls确认产物、gdb加bt | 链接期看符号名,运行期看调用栈 |
这套表的价值在于:报错文本里出现VERR_前缀就绝对不要去改代码,出现undefined reference就绝对不要去重装虚拟机。方向错了,试一晚上也不会对。
第二个习惯是每次都把完整的报错文本留下来。虚拟机的错误弹窗有个「复制」按钮,终端报错直接选中复制,别只截个图。很多报错的第一行是通用描述,真正的原因藏在后面几行的括号里,比如(VERR_VMX_NO_VMX)这个后缀才指明了是虚拟化被占用,而不是虚拟机文件有问题。
第三,改完一处只验证一处。装完增强功能就ps aux | grep -i vbox确认,再往下走;cmake ..过了就ls看看 Makefile 生成了没有,再make。一次改三四个地方,出问题时根本不知道是哪一步引入的新问题,这是我在这种环境类故障上踩过最深的坑。
最后是保留可回退状态。建虚拟机之前给 vdi 单独放一个目录,装完增强功能拍一次快照,跑通第一次编译再拍一次。真出问题时,回滚十秒,比重装一小时划算。从那以后我每次配这类一次性环境,都是先拍快照再动手,宁可多占几个 GB 磁盘,也不想在截止前一小时对着一个起不来的虚拟机干瞪眼。希望帮到你。
本文还有配套的精品资源,点击获取