前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一道华为C语言面试题,很多人都栽了!

一道华为C语言面试题,很多人都栽了!

作者头像
轩辕之风
发布2024-04-26 20:52:07
810
发布2024-04-26 20:52:07
举报
文章被收录于专栏:编程技术宇宙编程技术宇宙

大家好,我是轩辕。

周末的一天,我的从零开始学逆向学习群里有人抛出了一个C语言相关的问题:

先想一想,这段代码运行后会输出什么?

这道题我几年前在华为的面试题中也遇到过。

代码很简短,main函数定义了一个指针变量p,然后将其地址传递给fun函数,fun函数使用malloc函数在堆上分配了100个字节的空间,并把这块内存的地址赋值给了p。回到main函数中,紧接着调用free函数释放刚刚分配的内存。

随后来了一个if判断,如果指针p不等于NULL,则使用strcpy向p所在的内存拷贝一个"hello world"字符串,随后调用printf函数将其打印输出。

看到这里,你应该明白了,这是一道非常典型的悬空指针问题。注意,不是有些人认为的野指针,野指针是定义的指针变量未曾初始化赋值。而悬空指针才是上面这种,已经释放后,但又没有及时将其置为NULL。

C语言中的指针如果使用不当,经常容易出现这类指针的问题,这也是很多人觉得C语言指针难打交道的原因之一。

所以,从一开始学习C语言的时候,就会有人给你强调,刚刚定义的指针一定要赋值,释放后的指针一定要置为NULL。所以C语言中一般不推荐直接调用free函数,而是通过一个宏定义来把这个过程自动化,编程的时候通过这个宏来释放指针,一定程度上避免因为编程习惯引入的悬空指针问题。

代码语言:javascript
复制
#define FREE(p) free(p); \
                p = NULL;

而在C++中,为了解决这个问题,引入了智能指针,把指针包在一个C++对象中,通过对象自动化析构的特点,从而完成上面的工作。

回到上面的题目中来,我们姑且不论malloc是否能成功分配到内存的问题,100个字节的空间,没有意外的情况下,99.99%的情况都能成功分配到。

而后通过free释放了内存,但指针变量p没有及时置空,仍然还是指向着这片内存地址,所以下面的if判断也一定是成立的,所以程序会进入到if中去。但p指向的这片内存已经被回收了,这时候使用strcpy向其写入数据,到底会造成什么后果就难以预料了。

运气好的话,字符串能够成功复制,也能成功打印出"hello world"字符串,比如我在VS2008下,用Debug模式运行:

运气不好,运行就会报错,什么也没有输出。比如同样在VS2008,换成Release模式:

现在你再猜一下,崩溃是在哪一行呢?

是strcpy写入数据的时候崩溃,还是printf打印输出的时候崩溃呢?

答案是printf的时候崩溃了,我们可以用WinDbg调试器来调试运行,发现strcpy运行并没有报错,成功把字符串完成了复制:

而通过查看崩溃时候的调用堆栈,实际是崩溃在了printf函数内部的调用链条上:

这是为什么呢?

实际上是这样的:虽然通过调用free把这块内存释放了,但要注意,这个释放只是C语言运行时库层面的释放(因为free函数是C语言的库函数),C语言运行时库里的算法把它回收回去,在编程语言的层面上,这块内存是不应该再访问的了。

但在操作系统的层面上,这块内存依然是可以访问的,它依然位于某个具有可读可写的4KB内存页中。因为C语言的堆内存分配算法,不会每次释放内存都调用系统级的函数(如VirtualFree)去真正释放内存页面,这是一个很重的操作。

这里所谓的free,仅仅是告诉C语言运行时库,这块内存我不用了,你回收回去统一管理吧。

所以,当调用strcpy的时候,是能够正常复制的。

但要注意,这块内存能写,不代表你能乱写。在操作系统层面上,内存页面可读可写,那你写没有问题。

但站在C语言运行时库的视角来看,这个地址的内容我已经回收了,现在这里面的内容对于我管理堆内存非常重要,你别乱写,乱写是要出乱子的。

这不,这样一strcpy,哦豁,堆内存里面的一些管理用的设施被破坏了(比如一些指针),等到后面调用printf的时候,里面同样要从堆分配内存,这个时候前面留下的问题就暴露出来了。

但如果你把printf换成MessageBox函数,还是能正常弹窗的:

这是因为MessageBox是Win32的API函数,它的调用不涉及到C语言运行时库的操作,C语言的堆被搞坏了,跟它没有关系。

不过,当你点击上面的弹窗消息后,程序依然会提示你报错。这是因为main函数返回后,程序的流程又会进入到C语言运行时库的地盘,堆内存被破坏的事情这个时候还是会被捅出来。

那为什么Debug模式下,程序又能够成功运行呢?这可能有两方面的原因:

1、Debug和Release模式下,C语言运行时库管理堆内存的方法有些差异。可能strcpy写入的内容并没有破坏堆管理算法的一些关键数据结构。

2、确实破坏了,但后面C语言运行时库工作的时候没有触发这个问题。

至于具体是哪一种原因,还得要深入研究C语言运行时库的堆内存管理算法,结合调试分析才能下结论了。

另外,这段代码在Linux上默认编译后,也是能够运行的:

所以总结来看,这段代码能不能正常工作,没有一个确定的说法,与不同的平台、不同的编译模式都有关系,它的运行结果是不确定的。

释放后使用攻击

说到悬空指针,顺便给大家延伸一点,来看下面这段代码:

我先给指针p分配了100个字节,里面填充了"hello, world"之后,打印输出,随后释放指针p的内存。

但要注意,我释放后,同样没有把p置空。

紧接着,我又调用malloc分配了100个字节给指针q,随后给它指向的内存填充了"hello, xuanyuan"。

但好玩的来了,我接下来还是打印p,不是打印q,居然把指针q的内容给我打印出来了。

打印了两次p,两次输出的内容居然不一样,这是为什么呢?

调试一下就会发现,现在p和q两个指针指向的地址是一样的,都指向了同一块内存:

这是利用了C语言运行时库堆内存分配算法的特点,把上面刚刚free归还的100个字节,又分配给新的q了,而p又还没有置空,就出现了p和q同时指向了这块内存。

而这个特性,常常被应用在在二进制安全攻击里面。有一种攻击手法叫做释放后使用攻击(Use After Free),简称UAF,就是用的这一招。

明明现在的内存是人家q的,但p也指向了它,会出什么事情呢?

假如p原来指向的是一个结构体,里面有个函数指针,通过p->fun()可以调用。

现在我通过这种方式创建了一个假的结构体,里面有恶意代码的函数指针,这样p->fun()调用的就是恶意代码了!

一个小小的指针,背后的故事可不简单哦!

今天的文章有收获吗,欢迎大家转发分享收藏,你的支持是我更新的动力哦!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程技术宇宙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 释放后使用攻击
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档