最近準備從 C 語言零基礎到 PHP 擴展開發實戰,案例的過程當中準備了以下代碼碎片,演示解析http scheme
php
#include <stdio.h> #include <stdlib.h> #include <string.h> char *parse_scheme(const char *url) { char *p = strstr(url,"://"); return strndup(url,p-url); } int main() { const char *url = "http://static.mengkang.net/upload/image/2019/0907/1567834464450406.png"; char *scheme = parse_scheme(url); printf("%s\n",scheme); free(scheme); return 0; }
上面是經過strndup
的方式,背後也依託了malloc
,因此最後也須要free
。
有人在微信羣私信parse_scheme
能用char []
來作返回值嗎?咱們知道棧上的數組也能用來存儲字符串,那咱們能夠改寫成下面這樣嗎?segmentfault
char *parse_scheme(const char *url) { char *p = strstr(url,"://"); long l = p - url + 1; char scheme[l]; strncpy(scheme, url, l-1); return scheme; }
大多數人都知道不能這樣寫,由於返回的是棧上的地址,當從該函數返回以後,那段棧空間的操做權也釋放了,當再次使用該地址的時候,值就是不肯定的了。數組
那咱們今天就一塊兒探討下出現這樣狀況的背後的真正原理。bash
每一個函數運行的時候由於須要內存來存放函數參數以及局部變量等,須要給每一個函數分配一段連續的內存,這段內存就叫作函數的棧幀(Stack Frame)。
由於是一塊連續的內存地址,因此叫幀;爲何叫要加一個棧
呢?
想必你們都熟悉了函數調用棧,爲何叫函數調用棧呢?好比下面的表達式微信
array_values(explode(",",file_get_contents(...)));
函數的執行順序是最內層的函數最早執行,而後依次返回執行外層的函數。因此函數的執行就是利用了棧的數據結構,因此就叫棧幀。數據結構
x86_64 cpu上的 rbp
寄存器存函數棧底地址,rsp
寄存器存函數棧頂地址。函數
#include <stdio.h> void foo(void) { int i; printf("%d\n", i); i = 666; } int main(void) { foo(); foo(); return 0; }
$gcc -g 2.c $./a.out 0 666
爲何第二次調用foo
函數輸出的結果都是上次函數調用的賦值呢?先看下反彙編以後的代碼url
000000000040052d <foo>: #include <stdio.h> void foo(void) { 40052d: 55 push %rbp 40052e: 48 89 e5 mov %rsp,%rbp 400531: 48 83 ec 10 sub $0x10,%rsp int i; printf("%d\n", i); 400535: 8b 45 fc mov -0x4(%rbp),%eax 400538: 89 c6 mov %eax,%esi 40053a: bf 00 06 40 00 mov $0x400600,%edi 40053f: b8 00 00 00 00 mov $0x0,%eax 400544: e8 c7 fe ff ff callq 400410 <printf@plt> i = 666; 400549: c7 45 fc 9a 02 00 00 movl $0x29a,-0x4(%rbp) } 400550: c9 leaveq 400551: c3 retq 0000000000400552 <main>: int main(void) { 400552: 55 push %rbp 400553: 48 89 e5 mov %rsp,%rbp foo(); 400556: e8 d2 ff ff ff callq 40052d <foo> foo(); 40055b: e8 cd ff ff ff callq 40052d <foo> return 0; 400560: b8 00 00 00 00 mov $0x0,%eax } 400565: 5d pop %rbp 400566: c3 retq 400567: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 40056e: 00 00
第一次進入 foo
函數先後spa
在進入foo
函數以前,由於main
裏沒有參數也沒有局部變量,因此,main 的棧幀的長度就是0,rbp
和rsp
相等(0x7fffffffe2c0
)。當執行.net
callq 40052d <foo>
會把main
函數的在調用foo
以後須要返回執行的下一行代碼的地址壓棧,由於是64位機器,地址8字節。
進入foo
以後
push %rbp
把rbp
的值壓棧,由於也是存的地址,因此又佔了8字節,因此當初始化foo
函數的rbp
的時候
mov %rsp,%rbp
rsp
已經在原來的基礎上加了16
字節,因此從0x7fffffffe2c0
變成了0x7fffffffe2b0
。
sub $0x10,%rsp
由於foo
函數裏面局部變量,編譯的時候就預留了16
字節,因此rsp
變爲了0x7fffffffe2a0
最後執行了
movl $0x29a,-0x4(%rbp)
將666
放在了0x7fffffffe2ac
,當第二次調用的時候,打印i
的彙編代碼以下
printf("%d\n", i); 400535: 8b 45 fc mov -0x4(%rbp),%eax 400538: 89 c6 mov %eax,%esi 40053a: bf 00 06 40 00 mov $0x400600,%edi 40053f: b8 00 00 00 00 mov $0x0,%eax 400544: e8 c7 fe ff ff callq 400410 <printf@plt>
第二次進入 foo
函數先後
由於上次-0x4(%rbp)
存了666
,而第二次調用foo
的rbp
的值又和第一次同樣,因此是一個地址。因此666
就被打印出來了。
#include <stdio.h> #include <stdlib.h> #include <string.h> char *parse_scheme(const char *url) { char *p = strstr(url,"://"); long l = p - url + 1; char scheme[l]; strncpy(scheme, url, l-1); printf("%s\n",scheme); return scheme; } int main() { const char *url = "http://static.mengkang.net/upload/image/2019/0907/1567834464450406.png"; char *scheme = parse_scheme(url); printf("%s\n",scheme); return 0; }
調試信息以下,當從parse_scheme
返回時,打印scheme
的結果仍是http
,可是當咱們調用printf
以後,和上面樣例中同樣,parse_scheme
出棧,printf
入棧,則棧上內存就又替換了,因此打印出來的結果則不必定是http
了。