我们先使用字符指针,声明字符串,并修改其中元素:
#include <stdio.h>
int main() {
char *name = "Sam";
name[0] = 'J';
printf("%s\n", name);
return 0;
}
使用 gcc 编译执行后,报错误:段错误 (核心已转储),英文的话是 Segmentation fault (core dumped):
gcc main.c -o main
./main
段错误 (核心已转储)
再使用字符数组,声明字符串,并修改其中元素:
#include <stdio.h>
int main() {
char name[] = "Sam";
name[0] = 'J';
printf("%s\n", name);
return 0;
}
用 gcc 编译执行后一切正常:
gcc main.c -o main
./main
Jam
那么字符指针和字符数组究竟有什么区别?为什么第一段代码会出段错误?
字符指针
我们把第一段代码反汇编来看一下,反汇编的命令是 objdump -d exec_file_name
:
objdump -d main
只关注 main 函数部分:
0000000000400526 <main>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 48 83 ec 10 sub $0x10,%rsp
40052e: 48 c7 45 f8 d4 05 40 movq $0x4005d4,-0x8(%rbp)
400535: 00
400536: 48 8b 45 f8 mov -0x8(%rbp),%rax
40053a: c6 00 4a movb $0x4a,(%rax)
40053d: 48 8b 45 f8 mov -0x8(%rbp),%rax
400541: 48 89 c7 mov %rax,%rdi
400544: e8 b7 fe ff ff callq 400400 <puts@plt>
400549: b8 00 00 00 00 mov $0x0,%eax
40054e: c9 leaveq
40054f: c3 retq
main 函数所在地址是 0000000000400526
,可以看出我这边是 64 位操作系统,地址占用 8 个字节。下文的地址为了简洁,都省略地址前面的 0,简化为 0x400526
。
我们先看字符指针的赋值 char *name = "Sam"
:
40052e: 48 c7 45 f8 d4 05 40 movq $0x4005d4,-0x8(%rbp)
这句话的意思是,查找 0x4005d4
地址上的值,并转移到 rbp 寄存器中。
那么这个地址上的存储的值是什么呢?我们可以使用 objdump -s exec_file_name
命令,查看可执行文件中的所有段:
objdump -s main
main: 文件格式 elf64-x86-64
...省略...
Contents of section .rodata:
4005d0 01000200 53616d00 ....Sam.
...省略...
可以看到 "Sam" 被编译器放到了 .rodata 段,ro 代表 read only,这个段的属性是只读的,0x4005d0
表示这一行的地址,字符 'S' 对应的 ASCII 是 0x53
,前面的 01000200
是4个字节,所以刚好在 0x4005d0 + 4 = 0x4005d4
的位置。
对于 char *name = "Sam"
,就是将字符串 "Sam" 的首地址 0x4005d4
赋值给 name 这个字符指针,而 name 存放在 main 函数栈的 -0x8(%rbp)
这个位置上。
那段错误是如何产生的呢?
这就要看修改元素的代码 name[0] = 'J'
:
400536: 48 8b 45 f8 mov -0x8(%rbp),%rax
40053a: c6 00 4a movb $0x4a,(%rax)
-0x8(%rbp)
存储的是 name,也就是上面说的地址 0x4005d4
,先把这个地址放在 rax 寄存器中,然后寻址 rax 寄存器的内容为地址的地址空间,也就是 0x4005d4
对应的地址空间,也就是字符 'S' 的位置,程序尝试向这个位置写入 0x4a
,也就是 'J' 字符。
上面我们已经说了, 0x4005d4
这个地址属于只读数据段,必然不允许写入,所以这个写入操作就是一个非法的访问,直接触发了段错误。
字符数组
那么对于 char name[] = "Sam"
又是什么情况呢?
我们也来反汇编一下:
0000000000400596 <main>:
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 10 sub $0x10,%rsp
40059e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4005a5: 00 00
4005a7: 48 89 45 f8 mov %rax,-0x8(%rbp)
4005ab: 31 c0 xor %eax,%eax
4005ad: c7 45 f0 53 61 6d 00 movl $0x6d6153,-0x10(%rbp)
4005b4: c6 45 f0 4a movb $0x4a,-0x10(%rbp)
4005b8: 48 8d 45 f0 lea -0x10(%rbp),%rax
4005bc: 48 89 c7 mov %rax,%rdi
4005bf: e8 9c fe ff ff callq 400460 <puts@plt>
4005c4: b8 00 00 00 00 mov $0x0,%eax
4005c9: 48 8b 55 f8 mov -0x8(%rbp),%rdx
4005cd: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
4005d4: 00 00
4005d6: 74 05 je 4005dd <main+0x47>
4005d8: e8 93 fe ff ff callq 400470 <__stack_chk_fail@plt>
4005dd: c9 leaveq
4005de: c3 retq
4005df: 90 nop
我们先看字符数组的赋值 char name[] = "Sam"
:
4005ad: c7 45 f0 53 61 6d 00 movl $0x6d6153,-0x10(%rbp)
需要注意的是,这里的 0x6d6153
并不是一个地址,0x6d
是字符 'm',0x6d6153
翻译成 ASCII 就是 "Sam"(注意字节序,这里是Little-Endian)。这句话就是给 main 函数栈的 -0x10(%rbp)
起始位置存入 "Sam"。
接下来看修改元素的代码 name[0] = 'J'
4005b4: c6 45 f0 4a movb $0x4a,-0x10(%rbp)
参考上面 main 函数栈中的 name 的位置,-0x10(%rbp)
就是 name[0]
,0x4a
就是 'J' 字符。
到这里,我们可以看出与上面操作字符指针的区别:这里操作的不是地址,而是字符。
那这次 "Sam" 存储在什么位置呢?我们同样使用 objdump -s exec_file_name
,查看可执行文件中的所有段:
objdump -s main
main: 文件格式 elf64-x86-64
...省略...
Contents of section .text:
4004a0 31ed4989 d15e4889 e24883e4 f0505449 1.I..^H..H...PTI
4004b0 c7c05006 400048c7 c1e00540 0048c7c7 ..P.@.H....@.H..
4004c0 96054000 e8b7ffff fff4660f 1f440000 ..@.......f..D..
4004d0 b8471060 0055482d 40106000 4883f80e .G.`.UH-@.`.H...
4004e0 4889e576 1bb80000 00004885 c074115d H..v......H..t.]
4004f0 bf401060 00ffe066 0f1f8400 00000000 .@.`...f........
400500 5dc30f1f 4000662e 0f1f8400 00000000 ]...@.f.........
400510 be401060 00554881 ee401060 0048c1fe .@.`.UH..@.`.H..
400520 034889e5 4889f048 c1e83f48 01c648d1 .H..H..H..?H..H.
400530 fe7415b8 00000000 4885c074 0b5dbf40 .t......H..t.].@
400540 106000ff e00f1f00 5dc3660f 1f440000 .`......].f..D..
400550 803de90a 20000075 11554889 e5e86eff .=.. ..u.UH...n.
400560 ffff5dc6 05d60a20 0001f3c3 0f1f4000 ..].... ......@.
400570 bf200e60 0048833f 007505eb 930f1f00 . .`.H.?.u......
400580 b8000000 004885c0 74f15548 89e5ffd0 .....H..t.UH....
400590 5de97aff ffff5548 89e54883 ec106448 ].z...UH..H...dH
4005a0 8b042528 00000048 8945f831 c0c745f0 ..%(...H.E.1..E.
4005b0 53616d00 c645f04a 488d45f0 4889c7e8 Sam..E.JH.E.H...
4005c0 9cfeffff b8000000 00488b55 f8644833 .........H.U.dH3
4005d0 14252800 00007405 e893feff ffc9c390 .%(...t.........
4005e0 41574156 4189ff41 5541544c 8d251e08 AWAVA..AUATL.%..
4005f0 20005548 8d2d1e08 20005349 89f64989 .UH.-.. .SI..I.
400600 d54c29e5 4883ec08 48c1fd03 e817feff .L).H...H.......
400610 ff4885ed 742031db 0f1f8400 00000000 .H..t 1.........
400620 4c89ea4c 89f64489 ff41ff14 dc4883c3 L..L..D..A...H..
400630 014839eb 75ea4883 c4085b5d 415c415d .H9.u.H...[]A\A]
400640 415e415f c390662e 0f1f8400 00000000 A^A_..f.........
400650 f3c3
...省略...
可以看到 "Sam" 放在了 .text 段中,地址分别是:0x4005b0
、0x4005b1
、0x4005b2
。
我们知道 .text 段也是只读的,那为什么这次操作没有出现段错误呢?
字符指针和字符数组的区别
-
char *name = "Sam";
是字符指针,得到的是一个,指向只读空间 "Sam",所在位置的指针变量。 -
char name[] = "Sam";
是字符数组,得到的是一个栈中的数组,数组中的三个地址分别指向 .text 段中 'S','a','m' 三个字符。当我们修改其中元素时,并没有修改 .text 段中的字符,只是修改了数组中的地址的指向,指向了 .text 段中的另一个字符。
字符指针和字符数组作为函数参数
那函数的形参写 char *str
和 char str[]
有区别吗?
对于 C 语言来说,声明一个函数的参数是数组的时候,实际上得到的是一个指针。C 语言没有传递数组的方式,通常都是以指针的形式传递。所以 char *str
和char str[]
作为函数形参是没有区别的。
例如:
#include <stdio.h>
void func1(char *str) {
str++;
printf("func1: %s\n", str);
}
void func2(char str[]) {
str++;
printf("func2: %s\n", str);
}
int main() {
char *a = "abc";
func1(a);
char b[] = "abc";
func2(b);
return 0;
}
编译执行:
gcc main.c -o main
./main
func1: bc
func2: bc
来看看参数是怎么传递和存储的:
0000000000400596 <func1>:
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 10 sub $0x10,%rsp
40059e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
4005a2: 48 83 45 f8 01 addq $0x1,-0x8(%rbp)
4005a7: 48 8b 45 f8 mov -0x8(%rbp),%rax
4005ab: 48 89 c6 mov %rax,%rsi
4005ae: bf d4 06 40 00 mov $0x4006d4,%edi
4005b3: b8 00 00 00 00 mov $0x0,%eax
4005b8: e8 b3 fe ff ff callq 400470 <printf@plt>
4005bd: 90 nop
4005be: c9 leaveq
4005bf: c3 retq
00000000004005c0 <func2>:
4005c0: 55 push %rbp
4005c1: 48 89 e5 mov %rsp,%rbp
4005c4: 48 83 ec 10 sub $0x10,%rsp
4005c8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
4005cc: 48 83 45 f8 01 addq $0x1,-0x8(%rbp)
4005d1: 48 8b 45 f8 mov -0x8(%rbp),%rax
4005d5: 48 89 c6 mov %rax,%rsi
4005d8: bf df 06 40 00 mov $0x4006df,%edi
4005dd: b8 00 00 00 00 mov $0x0,%eax
4005e2: e8 89 fe ff ff callq 400470 <printf@plt>
4005e7: 90 nop
4005e8: c9 leaveq
4005e9: c3 retq
00000000004005ea <main>:
4005ea: 55 push %rbp
4005eb: 48 89 e5 mov %rsp,%rbp
4005ee: 48 83 ec 20 sub $0x20,%rsp
4005f2: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4005f9: 00 00
4005fb: 48 89 45 f8 mov %rax,-0x8(%rbp)
4005ff: 31 c0 xor %eax,%eax
400601: 48 c7 45 e8 ea 06 40 movq $0x4006ea,-0x18(%rbp)
400608: 00
400609: 48 8b 45 e8 mov -0x18(%rbp),%rax
40060d: 48 89 c7 mov %rax,%rdi
400610: e8 81 ff ff ff callq 400596 <func1>
400615: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
40061c: 48 8d 45 f0 lea -0x10(%rbp),%rax
400620: 48 89 c7 mov %rax,%rdi
400623: e8 98 ff ff ff callq 4005c0 <func2>
400628: b8 00 00 00 00 mov $0x0,%eax
40062d: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400631: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
400638: 00 00
40063a: 74 05 je 400641 <main+0x57>
40063c: e8 1f fe ff ff callq 400460 <__stack_chk_fail@plt>
400641: c9 leaveq
400642: c3 retq
400643: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40064a: 00 00 00
40064d: 0f 1f 00 nopl (%rax)
首先 a 和 b 两个字符串的首地址是放在 main 函数栈的 -0x18(%rbp)
和 -0x10(%rbp)
这个位置的:
400601: 48 c7 45 e8 ea 06 40 movq $0x4006ea,-0x18(%rbp)
...
400615: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
然后在调用 func1(a)
和 func2(b)
前,程序将这个指针保存在 rdi 寄存器里:
400609: 48 8b 45 e8 mov -0x18(%rbp),%rax
40060d: 48 89 c7 mov %rax,%rdi
...
40061c: 48 8d 45 f0 lea -0x10(%rbp),%rax
400620: 48 89 c7 mov %rax,%rdi
然后就是调用 func1(char *str)
和 func2(char str[])
。进入 func1
或 func2
时先是一顿栈操作,预留了栈空间。
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 10 sub $0x10,%rsp
然后将上面说的字符串的地址保存在 rdi 寄存器里,接着 func1
和 func2
就把这个地址从 rdi 寄存器里取出来,保存到各自的栈 -0x8(%rbp)
的位置。
40059e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
注意这里两个函数的 -0x8(%rbp)
都是相对于各自栈来说,是两个不同的位置,而且和 main 函数的 -0x8(%rbp)
也是不同的。
然后对各自的变量进行 str++
操作:
4005a2: 48 83 45 f8 01 addq $0x1,-0x8(%rbp)
可以看到,不管是将参数写成 char *str
还是 char str[]
,编译出来的都没有区别。当然不同的编译器以及不同的编译选项偶可能造成不同的编译结果,但是总体原理是一样的。
总结
- 使用
char *name = "Sam"
代码,name 指针指向的实际上是只读区域,因此,更准确的写法是const char *name = "Sam"
。 - 使用
char name[] = "Sam"
代码,编译器会生成一段初始化数组的代码,该数组存储在栈中,name 指针指向栈中的数组位置,数组的内容当然是可以修改的。 - 如果不需要修改数组的内容,使用
char *name = "SamFF"
写法效率会更高。 - 函数形参中使用
char *str
和char str[]
是没有区别的,都是字符指针。
网友评论