负责助教 jiajundu@bupt.edu.cn
print在各个语言中都是必不可少的一个函数。例如,在C语言中,printf常常被用来输出一些信息或者用来debug,在C++中,cout承担类似的角色,但是作为一个代码中的隐藏第六人,却会影响代码的执行结果,比如,经常会遇到注释或者增加一个cout,导致程序执行结果错误或者segment fault。这些诡异的bug都是因为对cout在内存模型的影响不够了解,我们这个作业就用一个简化后的例子浅探一下glibc里cout的世界,并且借助本次实验让大家学习使用gdb进行调试的方法。
本系列实验为了保证环境的一致性、方便大家实验,我们每个实验都会搭建一个devcontainer作为实验环境。
实验环境的配置分为下面几个部分:
- x86-64 linux环境
- gcc,make
因此如果你有linux环境,例如wsl或者云服务器,可以直接在上面进行实验。
对于使用windows的同学仅需要安装docker和vscode devcontainer插件即可。
github上的code space也可以用,但并没有测试过。
参考链接:
如果你在Windows下使用dev container的插件,并且安装了wsl,可能需要手动将插件版本等级降级到v0.266.1
参考
选择较低版本后,reload window即可。
- 推荐参考这个链接: 安装docker
- 也可以在vscode里按ctrl+shift+P,输入
install,会出现安装 Docker的选项
如果安装过程中遇到了下面的问题,可能需要安装一下wsl2,可以参考链接1和链接2。
参考链接在windows上安装git,然后使用下面的命令clone本项目。
也可以从Github上下载整个项目的压缩包,但最好是clone,这样有更新就可以拉取到了。
git clone https://github.com/BUPT-OS/easy_lab_spring.git
# 网络访问有问题的同学也可以clone gitee镜像,两个仓库是同步的:
# git clone https://gitee.com/tobeape6ehok/easy_lab_spring.git打开的时候.devontainer要在第一层文件夹:
按ctrl+shift+P或者F1,输入reopen.. 选择下面这个选项即可使用dev container

在lab1的分支中含有print.cpp文件,文件中的代码包含一个很奇怪、有趣的现象:我们在23行通过调用cout打印了一个字符串,如果我们在程序中注释掉这一行,程序在运行时就会崩溃,如果解开注释,程序就能够执行成功。
你可以参考下面测试方法一节来验证这个奇怪的现象。
很显然,这个奇怪的现象背后肯定有一个bug在作怪。
所以,lab1的第一个任务就是:在注释掉23行的情况下,找出导致程序崩溃的bug,并且修复它,使得程序可以正常运行。
完成任务1其实只需要更改
print.cpp中一处即可。
在确定bug已经被成功修复之后,你需要生成git patch,提交到评测平台来验证bug是否已经被修复。关于生成patch的方法,可以参考链接。
本次lab的本意其实是想让大家理解堆内存的管理方式、以及cout对于堆内存的影响,而方式就是通过分析任务1中的bug。
任务2中你需要分析原始的print.cpp文件,并且在评测平台上提交一个pdf格式的实验报告,内容包括:
- 在注释掉23行
cout后,程序是在执行到哪一行代码才崩溃的? (给出行号) - 从堆内存角度,在注释掉23行
cout后,解释bug产生的原因、以及程序崩溃的原因 - 从堆内存角度,说明为什么23行
cout在解开注释的情况下可以使得程序正常运行
由于本次实验涉及的内容比较隐秘,所以我们在本文后半部分添加了基础知识引导章节,旨在帮助大家明确思路并引导正确的思考方向。
对于任务2来说,大家注意思考程序的堆上都分配了哪些内存、这些内存之间的位置顺序是怎么样的
在任务1和任务2的基础上,我们在cout被注释、bug依然存在的情况下,添加一行代码(可以查看print_v2.cpp文件):
if(i == 8 || i == 9) continue;可以通过执行make print_v2命令编译运行该文件。该任务中你不需要修改代码,只需要回答以下问题:
- 程序在运行的过程中会有输出吗?
- 程序可以正常运行吗?
- 如果可以正常运行,解释为什么在bug依然存在的情况下可以正常运行?
- 如果不可以正常运行:
- 程序是在执行到哪一行代码才崩溃的? (给出行号)
- 解释此时导致程序崩溃的原因又是什么?
同样把对上面问题的回答写在pdf文档里一起提交到评测平台即可。
在lab仓库根目录下,我们提供了一个Makefile来编译、运行,在修改print.cpp之后,可以通过命令运行make all来进行测试,如果执行成功,则会输出Program execution successful.,如果执行失败,则会输出Program execution failed.。
提交方式:将patch和pdf文档提交到评测平台(链接见微信群通知),提交到平台后会自动运行代码并且评分。
评分规则:本lab满分100分,包含patch 10分 (提交patch后成功修复的情况下得到10分)、文档90分。
文档评分仅关注答案是否正确。
deadline: 2026.3.26 23:59
在c/c++中,用户程序使用malloc或者new向glibc中的内存分配器申请堆内存时,内存分配器首先会查看维护的空闲内存块是否可以满足请求,如果可以,则直接分配成功,如果不能满足请求,则内存分配器会首先通过系统调用向内核请求更多内存,然后再完成分配。
为了方便维护堆内存的信息,glibc的内存分配器(以及其他大多数的内存分配器实现)会在用户指定的堆内存大小的基础上多分配一块堆内存,这一块多余的内存区域就用来保存该堆内存的信息(比如块大小等等),被称为元数据(meta-data)。比如用户如果指定需要分配32B,内存分配器在空闲堆内存空间中并不仅仅分配出32B的内存,而是会分配的大小是32+sizeof(meta-data),前面sizeof(meta-data)的区域用来保存元数据,后面部分以指针的形式返回给用户使用。
除此之外,cout在执行过程中也会在堆内存上申请buffer来保存需要输出的内容。
为了验证上述机制,我们在clue_to_you目录下给出了一些程序,你可以根据运行结果或者自行编写程序来验证自己的想法。
# Makefile中也提供了clue_b、clue_c、clue_d的命令
make clue_a对于本lab来说,你可以考虑以下问题:
- 在你的Linux环境中,meta-data的大小是多少?
cout的buffer大小是多少?cout的buffer在多个cout调用间可以复用吗?
关于glibc中其他更多的细节还可以阅读引用[1],也可以尝试阅读glibc的内存分配器源代码[2] [3]。
gdb是一个功能非常强大的代码调试工具,因为本次lab的关注点在于内存,所以学会使用gdb观测程序的内存状态是非常有用的。
在使用gdb调式前需要重新编译程序,在编译命令中添加-g参数使得在最后的二进制文件中包含调试信息。
# 添加-g参数
g++ a.cpp -g -o agdb中提供了一个x命令可以把触发断点后内存的信息打印出来,比如x/2g a的含义就是:获取a指针指向的地址后面的内存,以8个字节为单位,输出前两个,打印的结果就会如下:
(gdb) x/2g a
0x5d86e7901ea0: 0x0000000000000000 0x0000000000000031
你可以使用该功能来查看meta-data中保存的信息是什么,甚至你还可以打印cout的缓冲区内存信息,来查看内容是不是已经打印出来的内容。
对于其他比较常用的gdb指令,可以阅读引用[4]或者gdb的官方文档。
[1] glibc文档: https://sourceware.org/glibc/wiki/MallocInternals
[2] glibc的_int_free函数:https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4154
[3] glibc的_int_malloc函数:https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3512
[4] gdb cheat sheet: https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
[5] reddit的相关讨论:https://www.reddit.com/r/C_Programming/comments/p10ol6/printf_before_memory_allocation_fixes_bug_whats/




