字符串在程序的哪里?

2019-12-17

这一篇分析字符串,字符串经常被使用,但是它的秘密也不少:

一、字符串的存储位置

C源程序(string1.c):

#include <stdio.h>
int main()
{
	puts("Hello, World!");
	return 0;
}

我们直接看可执行文件的反汇编结果:

[lqy@localhost temp]$ gcc -o string1 string1.c
[lqy@localhost temp]$ ./string1 
Hello, World!
[lqy@localhost temp]$ objdump -s -d string1 > string1.txt
[lqy@localhost temp]$ 

string1.txt 中的部分内容如下:

Contents of section .rodata:
 8048488 03000000 01000200 00000000 48656c6c  ............Hell
 8048498 6f2c2057 6f726c64 2100               o, World!.  
...
080483b4 <main>:

 80483b4:	55                   	push   %ebp
 80483b5:	89 e5                	mov    %esp,%ebp
 80483b7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483ba:	83 ec 10             	sub    $0x10,%esp
 80483bd:	c7 04 24 94 84 04 08 	movl   $0x8048494,(%esp)
 80483c4:	e8 27 ff ff ff       	call   80482f0 puts@plt
 80483c9:	b8 00 00 00 00       	mov    $0x0,%eax
 80483ce:	c9                   	leave
 80483cf:	c3                   	ret

可见 0x8048494 应该是 “Hello, World!” 的地址,然后发现在 .rodata 段中 0x8048488 + 12 处正好存储着 “Hello, World!” 的各个字符的ASCII码:’H’ 的 ASCII码是 0x48,而感叹号 ‘!’ 的 ASCII码 码是 0x21,最后编译器还自动的为我们添了个字符串结束符 0x00:只要是双引号括起来的字符串,编译器都会自动的为我们加结束符。

由此我们发现:

字符串和全局变量一样做为静态数据存储在可执行文件中,在使用的时候用常量地址来访问。

但是字符串被放在了 .rodata 段中,这个段中的数据与 .text 段(代码段)中的数据一样在可执行文件被载入内存运行时都是只读的(这里的只读是通过分页管理实现的:在页表表项中有一个位,设置为 1 表示该页可写,设置为 0 表示该页只读;如果试图向只读的页中写入数据,CPU 就会触发页保护异常)。

所以源字符串中的任何字符都不能在程序运行时更改。

二、字符串指针 和 字符数组

C源程序(string2.c):

#include <stdio.h>
int main()
{
	char *s1 = "1234567";
	char s2[]= "1234567";
	
	puts(s1);
	puts(s2);
	return 0;
}

可执行文件的反汇编结果的赋值部分如下:

 80483bd:	c7 44 24 1c c4 84 04 	movl   $0x80484c4,0x1c(%esp)
 80483c4:	08 
 80483c5:	a1 c4 84 04 08       	mov    0x80484c4,%eax
 80483ca:	8b 15 c8 84 04 08    	mov    0x80484c8,%edx
 80483d0:	89 44 24 14          	mov    %eax,0x14(%esp)
 80483d4:	89 54 24 18          	mov    %edx,0x18(%esp)

0x80484c4 是字符串”1234567”的地址,所以:常量字符串赋值给指针时传递的是源字符串的地址;而赋值给局部字符数组时,要当成数字一个个拷贝到局部变量空间。因此第2种方式既浪费空间(4字节 vs 8字节)又浪费时间(1个mov vs 4个mov)。

但是第2种方式也不是一无是处:第1种方式传递的是源字符串的地址,而源字符串在只读页中,无法修改,但是字符数组却可以修改。

经验:如果程序中本来就没想改动该字符串,那就用指针吧;否则用 字符数组 或 动态申请的空间 来存。

三、格式描述符 和 转义符

这个部分,我们来看看字符串中格式描述符和转义符的来龙去脉,C源程序(string3.c):

#include <stdio.h>
int main()
{
	printf("--------\n%d\n", 123);
	return 0;
}
汇编源文件中
gcc -S string3.c

结果如下:

.LC0:
	.string	"--------\n%d\n"
目标文件中
gcc -c string3.c
objdump -s -d string3.o > string3.txt

-c 默认输出到 string3.o 文件中,string3.txt 中的字符串:

Contents of section .rodata:
 0000 2d2d2d2d 2d2d2d2d 0a25640a 00        --------.%d..   

两个’\n’被替换成了 0x0a,%d 没变

可执行文件中
gcc -o string3 string3.c
objdump -s -d string3 > string3.txt

可执行文件跟目标文件一样:

Contents of section .rodata:
 80484a8 03000000 01000200 00000000 2d2d2d2d  ............----
 80484b8 2d2d2d2d 0a25640a 00                 ----.%d..       

所以我们知道了:转义符在变成二进制文件后就被转义为我们想表达的那个字符了,而格式描述符自然是要留给 printf 运行时用的

知道这个有什么用?如果我们来设计 printf 函数,那么转义字符我们就不用操心了,编译器会把它们转义过去的。