百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

CPU眼里的:字符串 vs 数组(字符数组与字符串区别)

liebian365 2025-03-26 13:49 7 浏览 0 评论

“它们十分相似,但又非常不同

01

提出问题

字符串和字符数组,在内存分布上,跟普通数组(例如:int类型的数组)有很高的相似性。但使用字符串的危险系数,却远远高于普通数组。是什么细微的差异导致了二者在使用上,有这么大的不同呢?暂时告别教条的标准答案,让我们一起掀开引擎盖,看看到底发生了什么?


02

数值特性

打开Compiler Explorer,编写一个常规的函数func1,里面定义了一个字符串a,它的初值是字符串“abc”;接着我们再定义一个函数func2,里面定义了一个字符数组b,它的初值分别是:a、b、c和0,其中0是字符串的结束符,由于这个结束符的存在,所以称数组b为字符串数组更为合适。具体代码如下所示:

void func1()
{
    char a[] = "abc";
}
void func2()
{
    char b[4] = {'a', 'b', 'c', 0 };
}

好了,比较一下二者的CPU指令,如图所示。

如你所见,它们对应的CPU指令完全相同!只是这个赋值语句有点让人费解,这不像是为数组a和b,赋值字符串,而是一个神奇的数字:6513249。

不用着急,让我们再写一个函数func3,这里是给数组c赋值,只是这里的初值,我们不写成字符,而是二进制数,具体代码如下:

void func3()
{
    char c[4] = { 0x61, 0x62, 0x63, 0x0 };
}

它所对应的CPU指令,如图所示。

通过它们完全一致的CPU指令,相信你也猜到了:6513249对应的16进制数正好是:0x00、0x63、0x62、0x61,这4个字节正好对应了:结束符'\0'、和字符c、b、a的ASCII码。由于x86 CPU是小端模式,所以这个顺序是倒着排的,具体原因,也可以参看“CPU眼里的大端、小端”

由此可见,无论人类世界的文字,多么妖娆,但在计算机的世界里面,它依然只是一个数字。我们不用纠结是用二进制数来初始化字符串,还是用字符来初始化数组,因为它们只是表示数的方法不同,并没有本质的区别。

对于资深读者的话,可能下面的函数func4更加原汁原味,它跟汇编指令符合的更好。

void func4()
{
    int x = 0x00636261;
}

之所以可以把数组写成int x,是因为数值0x00636261,无论是以数组的形式存在,还是以int类型存在,它们在内存中的分布是相同的。

不仅如此,有时候我也会看到用字符来初始化int类型的变量,例如:

void func5()
{
    int y = '\0cba';
}

其实函数func4和func5的本质相同,对应的CPU指令也完全一致,不同的只是表现的手法。如果只在语法层面记忆这些怪异规则的话,显然是非常突兀的,但如果从底层视角看它们的话,似乎一切又是丝滑和顺利成章的。

或许你也发现了:一条简单的汇编指令,其对应的高级语言实现方式是可以多种多样的。这也是逆向工程,往往不能精准还原源代码的原因之一,因为可选的还原路径确实太多了,至少字符串是这样的。


03

字符越界

在了解完字符的数值特征后,让我们再看看为什么字符串比数组更加容易越界?

这里我们定义了两个全局的字符串数组aa和bb,其中定义数组aa的时候,我们故意遗漏了“结束符”(\0),具体代码和运行结果如图所示。

如你所见,对于没有结束符的数组aa,在汇编文件中,其初值“abcd”,会被注明成:ASCII码;而对于有结束符的数组bb,其初值“efgh”才会被注明成:字符串(.string)。

最后,在main函数中,输出字符数组aa,并分别输出其表示的字符串长度和数组aa的长度。输出结果如上图右下角所示:尽管数组aa的size还是4个字节,但它所表示的字符串的长度则超过了它的size,达到了8个字节。数组aa所代表的字符串已经越界到数组bb了,此时aa代表的字符串不再是:abcd;而是:abcdefgh!

这个结果可能跟程序员的初衷是违背的,虽然不会立刻导致程序崩溃,但随着时间的推移,可能引发更多的逻辑错误,随着这些逻辑错误,距离字符串代码渐行渐远,可能导致开发者无法意识到:逻辑错误的根本原因是字符串,从而陡增了调试的难度。

再看看数组aa和bb的内存分布图,如图所示。

如你所见,由于字符数组aa和bb之间没有结束符(\0),所以在没有源代码的提示下,我们也无法知道这是两个字符数组。同样,由于我们只为库函数strlen输入了字符串的内存首地址,所以,库函数strlen也无法知道字符串数组aa的真实长度,除非遇到结束符(\0)。

当然不仅是库函数strlen,那些专门用来处理字符串的库函数,都会存在类似的问题。例如库函数:strlen(),strcmp(),strcpy(),strcat()…

不过有些时候,即使不加入结束符,编译器或者操作系统会将一些数据段初始化为0,这相当于为潜在的字符串加上“结尾符”(\0),这一定程度上可以防止字符串因为遗漏“结束符”导致的访问越界。但这也可能纵容了大家不规范的编码习惯,一旦没有编译器和操作系统兜底,就可能酿成字符串越界的错误。

为了应对这种安全问题,可以考虑改用更加安全的字符串库函数,例如:strnlen

size_t strnlen(const char *s, size_t maxlen);

它会要求开发者输入字符串的最大长度,从而减少字符串越界的机会。


04

字符串的访问属性

最后,我们来看一个非常隐蔽、且容易被大家忽视的字符串问题。在main函数中定义一个字符指针d,并将其赋值为字符串“xyz”;然后再把第一个字符'x',改写成字符'a'。改写的方式,既可以采用指针形式:*d = ‘a’,也可以采用数组形式:d[0] = ‘a’,二者背后的CPU指令是完全相同的。具体代码如下:

int main()
{
    char* d = "xyz";
    d[0] = 'a';
}

代码对应的CPU指令,如图所示。

虽然这两行代码十分简单,但却至少埋了两个大雷!第一个雷,是眼力大挑战:这里定义指针变量d的代码,跟定义一个数组的代码非常相似:char d[] = "xyz";但指针变量和数组变量,它们存储字符串的方式,有天壤之别!

对于指针变量d,它跟普通变量一样,是某一段内存地址的别名,从上图对应的CPU指令可以看出:变量d代表的内存首地址是:[rbp - 8],属于函数堆栈内存,在该内存里面,存储着字符串“abc”的内存首地址。

需要注意的是:字符串“xyz”本身并没用跟随变量d,也存储在函数的堆栈内存里面,相反它存储在全局数据段里面。

这样,字符串“xyz”的生命周期,不会像函数中的临时变量(栈变量)或临时数组a、b、c那样,随着函数的返回,其生命周期也随之结束;相反,字符串“xyz”的生命周期,将贯穿整个程序的运行过程。如果分别打印出指针变量d和字符串“xyz”的内存首地址的话,我们会发现它们之间有很大的距离,显然它们不是同一个存储区域。

第二个雷,就是写操作(读操作是被允许的),这里我们试图把第一个字符‘x’改成字符‘a’。运行结果如图所示。

程序居然崩溃了,segmentation fault!其实这段超简单的代码里面暗藏玄机。原来很多编译器会把字符串“xyz”安排在只读数据段。如果CPU集成了MMU,当我们试图对只读内存作写操作时,就会产生page fault,进而导致程序崩溃!没想到吧?如此简单的代码,竟然还涉及到了数据段和内存保护这类底层知识。这是不是非常超纲呢?

但对于没用MMU的单片机设备的话,可能不会导致程序崩溃,甚至系统对这种情况可能视而不见,毫无反应。但改写操作,却很有可能失败。不过这种写入失败,不会产生任何提示,这会让错误继续延续下去,直到出现了肉眼可见的逻辑错误。关于MMU的相关内容,可以参看章节“CPU眼里的:虚拟内存”

当然,现代编译器也会警告我们这个char*类型的变量d,跟const char*类型的字符串“xyz”是不匹配的,比较正确的写法应该是这样的:

const char* d = "xyz";
d[0] = 'a'; //error here

这样,当我们试图编写改变字符的代码时,编译器直接给出编译错误,从而禁止这种错误代码的运行。


05

总结

总的来说,字符也是一个数字。每个字符是一个特定的ASCII码,这跟普通变量表示是数字没有本质差异。字符串和字符数组的区别如下:

  1. 结束符:字符串以空字符(\0)结束,字符数组则不一定
  2. 初始化:字符串可以使用字符串字面量(例如"Hello")进行初始化,字符数组则需要逐个字符初始化或使用字符数组初始化。
  3. 处理方式:C标准库提供了许多处理字符串的函数,例如strlen、strcpy、strcmp等,这些函数依赖于字符串的结束符。而对于普通的字符数组,这些函数可能无法正常工作,除非字符数组恰好符合字符串的格式(即以结束符(\0)结束)。


06

更多知识

如果喜欢阿布这种解读方式,希望更加系统学习这些编程知识的话,也可以考虑看看由阿布亲自编写,并由多位微软大佬联袂推荐的新书《CPU眼里的C/C++》

【京东热卖】好评度:> 98%

【微信读书】推荐度:> 82%

<script type="text/javascript" src="//mp.toutiao.com/mp/agw/mass_profit/pc_product_promotions_js?item_id=7478862543725871650"></script>

相关推荐

go语言也可以做gui,go-fltk让你做出c++级别的桌面应用

大家都知道go语言生态并没有什么好的gui开发框架,“能用”的一个手就能数的清,好用的就更是少之又少。今天为大家推荐一个go的gui库go-fltk。它是通过cgo调用了c++的fltk库,性能非常高...

旧电脑的首选系统:TinyCore!体积小+精简+速度极快,你敢安装吗

这几天老毛桃整理了几个微型Linux发行版,准备分享给大家。要知道可供我们日常使用的Linux发行版有很多,但其中的一些发行版经常会被大家忽视。其实这些微型Linux发行版是一种非常强大的创新:在一台...

codeblocks和VS2019下的fltk使用中文

在fltk中用中文有点问题。英文是这样。中文就成这个样子了。我查了查资料,说用UTF-8编码就行了。edit->Fileencoding->UTF-8然后保存文件。看下下边的编码指示确...

FLTK(Fast Light Toolkit)一个轻量级的跨平台Python GUI库

FLTK(FastLightToolkit)是一个轻量级的跨平台GUI库,特别适用于开发需要快速、高效且简单界面的应用程序。本文将介绍Python中的FLTK库,包括其特性、应用场景以及如何通过代...

中科院开源 RISC-V 处理器“香山”流片,已成功运行 Linux

IT之家1月29日消息,去年6月份,中科院大学教授、中科院计算所研究员包云岗,发布了开源高性能RISC-V处理器核心——香山。近日,包云岗在社交平台晒出图片,香山芯片已流片,回片后...

Linux 5.13内核有望合并对苹果M1处理器支持的初步代码

预计Linux5.13将初步支持苹果SiliconM1处理器,不过完整的支持工作可能还需要几年时间才能完全完成。虽然Linux已经可以在苹果SiliconM1上运行,但这需要通过一系列的补丁才能...

Ubuntu系统下COM口测试教程(ubuntu port)

1、在待测试的板上下载minicom,下载minicom有两种方法:方法一:在Ubuntu软件中心里面搜索下载方法二:按“Ctrl+Alt+T”打开终端,打开终端后输入“sudosu”回车;在下...

湖北嵌入式软件工程师培训怎么选,让自己脱颖而出

很多年轻人毕业即失业、面试总是不如意、薪酬不满意、在家躺平。“就业难”该如何应对,参加培训是否能改变自己的职业走向,在湖北,有哪些嵌入式软件工程师培训怎么选值得推荐?粤嵌科技在嵌入式培训领域有十几年经...

新阁上位机开发---10年工程师的Modbus总结

前言我算了一下,今年是我跟Modbus相识的第10年,从最开始的简单应用到协议了解,从协议开发到协议讲解,这个陪伴了10年的协议,它一直没变,变的只是我对它的理解和认识。我一直认为Modbus协议的存...

创建你的第一个可运行的嵌入式Linux系统-5

@ZHangZMo在MicrochipBuildroot中配置QT5选择Graphic配置文件增加QT5的配置修改根文件系统支持QT5修改output/target/etc/profile配置文件...

如何在Linux下给zigbee CC2530实现上位机

0、前言网友提问如下:粉丝提问项目框架汇总下这个网友的问题,其实就是实现一个网关程序,内容分为几块:下位机,通过串口与上位机相连;下位机要能够接收上位机下发的命令,并解析这些命令;下位机能够根据这些命...

Python实现串口助手 - 03串口功能实现

 串口调试助手是最核心的当然是串口数据收发与显示的功能,pzh-py-com借助的是pySerial库实现串口收发功能,今天痞子衡为大家介绍pySerial是如何在pzh-py-com发挥功能的。一、...

为什么选择UART(串口)作为调试接口,而不是I2C、SPI等其他接口

UART(通用异步收发传输器)通常被选作调试接口有以下几个原因:简单性:协议简单:UART的协议非常简单,只需设置波特率、数据位、停止位和校验位就可以进行通信。相比之下,I2C和SPI需要处理更多的通...

同一个类,不同代码,Qt 串口类QSerialPort 与各种外设通讯处理

串口通讯在各种外设通讯中是常见接口,因为各种嵌入式CPU中串口标配,工业控制中如果不够还通过各种串口芯片进行扩展。比如spi接口的W25Q128FV.对于软件而言,因为驱动接口固定,软件也相对好写,因...

嵌入式linux为什么可以通过PC上的串口去执行命令?

1、uboot(负责初始化基本硬bai件,如串口,网卡,usb口等,然du后引导系统zhi运行)2、linux系统(真正的操作系统)3、你的应用程序(基于操作系统的软件应用)当你开发板上电时,u...

取消回复欢迎 发表评论: