当我们写程序时候难免会因为各种问题崩掉,如果是开发阶段,我们可以开gdb跟踪调试,但如果到了线上,就不能用gdb了,这时候我们可以把崩溃时候的调用栈信息打印出来,然后定位到具体崩溃的代码位置.
想要定位到具体的行号,需要在编译的时候加入-g参数,表示编译时候加入调试信息,调试信息里有相关的信息可以使地址转为行号.
下面介绍几个可以定位到崩溃位置的方法:
使用core文件
core文件其实是程序崩溃后的内存数据,也叫core dump或者dump文件,当得到core文件后就可以用gdb打开core文件,就能定位崩溃的位置了.
接下来我们先准备好一段测试代码,后面就用这段代码搞点事情
代码里我们故意除以0,使程序崩溃
float div(int a, int b){
float c = a/b;
return c;
}
int main(int argc, char** argv){
(void)argc;
(void)argv;
int a = 10;
int b = 0;
float c = div(a, b);
printf("div: %d/%d=%.2f\n", a, b, c);
return 0;
}
假如我们编译好的程序叫main,执行后,会显示Floating point exception,也就是除0错误了
bash$ ./main
Floating point exception (core dumped)
这时候看一下当前目录有没有生成core文件,默认应该是没有.
bash$ ls
main main.cpp main.o Makefile
当没有产生core文件的时候,就是开关没开,需要先打开开关
先查看一下ulimit -c的值
bash$ ulimit -c
0
上面显示结果为0,表示禁止生成core文件,下面我们设置为不限制core大小
bash$ ulimit -c unlimited
# 再用ulimit -c查看一下
bash$ ulimit -c
unlimited
然后再运行下程序看看,就生成core文件了
bash$ ./main
Floating point exception (core dumped)
bash$ ls
core main main.cpp main.o Makefile
接着用gdb打开core文件,打开就是崩溃的位置
bash$ gdb --core=./core main
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...done.
[New LWP 15220]
Core was generated by `./main'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x0000559cc0dbe143 in div (a=10, b=0) at main.cpp:5
5 float c = a/b;
(gdb)
可以bt看一下调用栈
(gdb) bt
#0 0x0000559cc0dbe143 in div (a=10, b=0) at main.cpp:5
#1 0x0000559cc0dbe182 in main (argc=1, argv=0x7ffd65f14f88) at main.cpp:15
上面说的ulimit -c unlimited这种方法在重启机器后就会失效,想永久打开生成core,需要修改些配置,这里大家可以下去自己研究一下,后面我们就说一下,假如没有core文件的时候,我们该怎么办.
在代码崩溃的时候把调用栈打印出来
core文件其实是靠不住的,因为core文件是程序的内存数据,当程序特别大的时候core文件就特别大,如果机器磁盘已经快满了,再想生成core文件就生不成了,这时候我们可以自己在程序里把程序崩溃时候的关键信息打印出来,就不需要太依赖core文件了
好,自己动手,丰衣足食.
想要打印调用栈,我们想到的就是bt这个词,也就是backtrace,也可以上网搜一下程序打印调用栈,得出来的结果就是用backtrace这个函数可以打印调用栈,我们可以man backtrace看一下这个函数,结果man里就有一段测试backtrace的代码,太爽了,man手册就是好用,里面有很多测试代码.
比如想学网络编程,就man一下socket,打开后,向下翻手册,如果没有看到测试代码就注意最后面有个SEE ALSO,这里会列出和socket相关的函数,挨个man这些函数,总有你想看到的测试代码,到时候拿来练习就可以了.
接下来我们就直接上码了,对着程序走一遍就会了
#include
#include
#include
#include
#include
#include
void myfunc3(void){
int j, nptrs;
#define SIZE 100
void *buffer[100];
char **strings;
nptrs = backtrace(buffer, SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
/* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
would produce similar output to the following: */
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++){
printf("%s\n", strings[j]);
}
free(strings);
}
void dump_backtrace(int signum){
printf("dump_backtrace on signal:%d\n", signum);
signal(signum,SIG_DFL);
myfunc3();
}
/* "static" means don't export the symbol... */
static void myfunc2(void){
myfunc3();
}
void myfunc(int ncalls){
if (ncalls > 1)
myfunc(ncalls - 1);
else
myfunc2();
}
void test_sig_segv(){
//这个就是段错误了
int* p = NULL;
printf("*p = %d\n", *p);
}
void test_sig_abort(){
abort();
}
void test_sig_abort_assert(){
assert( 0 );
}
void test_sig_abort_free(){
int* p = (int*)malloc(sizeof(int));
*p = 1;
printf("test_sig_abort_free: 0x%p %d\n", p, *p );
free(p);
printf("free once\n");
free(p);
printf("free twice\n");
}
void test_sig_fpe(){
int a = 10;
int b = 0;
int c = a / b;
printf("%d / %d = %d\n", a, b, c);
}
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
int pid = getpid();
printf("pid %d\n", pid);
#if 0
//这里是man手册里backtrace的测试代码,直接打印调用栈
if (argc != 2) {
fprintf(stderr, "%s num-calls\n", argv[0]);
exit(EXIT_FAILURE);
}
myfunc(atoi(argv[1]));
exit(EXIT_SUCCESS);
#else
//这里是先注册信号,当程序运行出错的时候,
//捕捉到信号后打印调用栈,
//实际使用中也是这种办法
signal(SIGSEGV, dump_backtrace); //segmentation violation
signal(SIGABRT, dump_backtrace); //abort program (formerly SIGIOT)
signal(SIGFPE, dump_backtrace); //floating-point exception
test_sig_segv();
//test_sig_abort();
//test_sig_abort_assert();
//test_sig_abort_free();
//test_sig_fpe();
#endif
return 0;
}
这里需要把Makefile也贴一下,注意下面加上了链接时候用的参数-rdynamic,这个参数的作用是让链接器把所有符号添加到动态符号表中,听起来比较抽象,后面咱们通过例子来看一下效果就明白了.
LINK = @echo linking $@ && g++
GCC = @echo compiling $@ && g++
GC = @echo compiling $@ && gcc
AR = @echo generating static library $@ && ar crv
FLAGS = -g -DDEBUG -W -Wall -fPIC
#FLAGS = -DDEBUG -W -Wall -fPIC
#FLAGS = -DNDEBUG -W -Wall -fPIC
#FLAGS = -g -DNDEBUG -W -Wall -fPIC
GCCFLAGS =
DEFINES =
HEADER = -I./
LIBS =
LINKFLAGS =
#注意这里不加-rdynamic的话,backtrace显示的只有地址,不能显示地址对应的符号
LINKFLAGS += -rdynamic
#LIBS += -lrt
#LIBS += -pthread
OBJECT := main.o \
BIN_PATH = ./
TARGET = main
$(TARGET) : $(OBJECT)
$(LINK) $(FLAGS) $(LINKFLAGS) -o $@ $^ $(LIBS)
.cpp.o:
$(GCC) -c $(HEADER) $(FLAGS) $(GCCFLAGS) -fpermissive -o $@ $<
.c.o:
$(GC) -c $(HEADER) $(FLAGS) -fpermissive -o $@ $<
install: $(TARGET)
cp $(TARGET) $(BIN_PATH)
clean:
rm -rf $(TARGET) *.o *.so *.a
上面的大多数代码都是从man backtrace里扒出来的,但是man手册里的代码是调用程序就直接打印调用栈了,和我们的需求不符,我们希望在程序崩溃的时候再打印,那就得改一下,程序崩溃的时候会触发一些相应的信号,我们只要在程序里捕捉到这些信号,然后在收到信号的时候再打印调用栈就可以了.
下面我们看看捕捉信号这几行代码
# 这部分代码里加了注释,下面的signal意思是注册一个信号,当发生该信号时,回调后面的函数,
# 下面是注册了三个信号,
# SIGSEGV是段错误,这错误就是指针相关的问题一般会引起该错误,比如取一个空指针的值
# SIGABRT是当程序用了abort()或者assert之类的函数时候会触发
# SIGFPE也就是除0时候发生的了
//这里是先注册信号,当程序运行出错的时候,
//捕捉到信号后打印调用栈,
//实际使用中也是这种办法
signal(SIGSEGV, dump_backtrace); //segmentation violation
signal(SIGABRT, dump_backtrace); //abort program (formerly SIGIOT)
signal(SIGFPE, dump_backtrace); //floating-point exception
然后我们再看一下dump_backtrace函数
#当程序崩溃后,我们把收到的信号值打了出来,
#然后用signal(signum,SIG_DFL)让程序默认处理该信号
#接着用myfunc3把调用栈打印了出来,实际项目中我们可以把这些信息写到日志里
void dump_backtrace(int signum){
printf("dump_backtrace on signal:%d\n", signum);
signal(signum,SIG_DFL);
myfunc3();
}
myfunc3里具体输出调用栈我们就不说了,都是抄来的东西,有兴趣的同学可以自己研究一下.
下面我们看一下程序运行效果,在编译的时候,咱们先把Makefile里的
LINKFLAGS += -rdynamic 这行注释掉
bash$ ./main
pid 15562
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x555d55255223]
./main(+0x1321) [0x555d55255321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f6585563100]
./main(+0x136c) [0x555d5525536c]
./main(+0x14ce) [0x555d552554ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f658554609b]
./main(+0x113a) [0x555d5525513a]
Segmentation fault (core dumped)
代码里我们用的是引起段错误的代码,所以信号为11,也就是SIGSEGV,下面是各信号的编号和含义
查看signal man手册,可以看到下面的signal定义。
1 SIGHUP terminate process terminal line hangup
2 SIGINT terminate process interrupt program
3 SIGQUIT create core image quit program
4 SIGILL create core image illegal instruction
5 SIGTRAP create core image trace trap
6 SIGABRT create core image abort program (formerly SIGIOT)
7 SIGEMT create core image emulate instruction executed
8 SIGFPE create core image floating-point exception
9 SIGKILL terminate process kill program
10 SIGBUS create core image bus error
11 SIGSEGV create core image segmentation violation
12 SIGSYS create core image non-existent system call invoked
13 SIGPIPE terminate process write on a pipe with no reader
14 SIGALRM terminate process real-time timer expired
15 SIGTERM terminate process software termination signal
16 SIGURG discard signal urgent condition present on socket
17 SIGSTOP stop process stop (cannot be caught or ignored)
18 SIGTSTP stop process stop signal generated from keyboard
19 SIGCONT discard signal continue after stop
20 SIGCHLD discard signal child status has changed
21 SIGTTIN stop process background read attempted from control terminal
22 SIGTTOU stop process background write attempted to control terminal
23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2))
24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2))
25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2))
26 SIGVTALRM terminate process virtual time alarm (see setitimer(2))
27 SIGPROF terminate process profiling timer alarm (see setitimer(2))
28 SIGWINCH discard signal Window size change
29 SIGINFO discard signal status request from keyboard
30 SIGUSR1 terminate process User defined signal 1
31 SIGUSR2 terminate process User defined signal 2
可以看到上面输出了程序的pid为15562,接着有./main(+0x1223)的字样,有好几行,每一行就是一层调用栈,最上面的是最后调用的函数,其实就是myfunc3()打印调用栈的函数,崩溃的地方在往下几行里面,这里我们仅介绍一下从地址得到函数的方法. 这里面的+0x1223就是代码的地址,我们需要用另一个工具把该地址翻译为代码行号.
./main(+0x1223) [0x555d55255223]
用addr2line把地址转为函数名和行号,注意只有编译时候加了-g参数的时候才会行到行号
# addr2line的参数说明
# -e表示指明程序名为main
# -s表示输入代码名的时候不要带路径
# -f表示显示函数名
# -C表示显示demangle后的函数名,demangle是针对mangle来说的,程序在编译后函数名都被转为函数符号了,该符号不便于我们识别,所以需要demangle一下
bash$ addr2line -e main 0x1223 -sfC
myfunc3()
main.cpp:14
# 下面是只显示代码文件和行号的参数
bash$ addr2line -e main 0x1223 -s
main.cpp:14
# 下面是没有经过demangle的函数名
bash$ addr2line -e main 0x1223 -sf
_Z7myfunc3v
main.cpp:14
也可以用gdb查看地址所在的函数名,但是这样查不到行号,没有addr2line好用
# 注意,gdb只能加载main,不要run,一run就无法解析地址了
Reading symbols from ./main...done.
(gdb) i symbol 0x1223
myfunc3() + 46 in section .text
(gdb)
小知识
可以用nm查看程序里的符号,比如我们nm一下main函数
bash$ nm main
U abort@@GLIBC_2.2.5
U __assert_fail@@GLIBC_2.2.5
U backtrace@@GLIBC_2.2.5
U backtrace_symbols@@GLIBC_2.2.5
0000000000004010 B __bss_start
0000000000004010 b completed.7931
w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000001140 t deregister_tm_clones
00000000000011b0 t __do_global_dtors_aux
0000000000003d60 t __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003d68 d _DYNAMIC
0000000000004010 D _edata
0000000000004018 B _end
U exit@@GLIBC_2.2.5
0000000000001544 T _fini
00000000000011f0 t frame_dummy
0000000000003d58 t __frame_dummy_init_array_entry
0000000000002384 r __FRAME_END__
U free@@GLIBC_2.2.5
U getpid@@GLIBC_2.2.5
0000000000003f58 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000000020e0 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003d60 t __init_array_end
0000000000003d58 t __init_array_start
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000001540 T __libc_csu_fini
00000000000014e0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
0000000000001460 T main
U malloc@@GLIBC_2.2.5
U perror@@GLIBC_2.2.5
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
0000000000001170 t register_tm_clones
U signal@@GLIBC_2.2.5
U __stack_chk_fail@@GLIBC_2.4
0000000000001110 T _start
0000000000004010 D __TMC_END__
0000000000001421 T _Z12test_sig_fpev
0000000000001358 T _Z13test_sig_segvv
00000000000012ec T _Z14dump_backtracei
0000000000001384 T _Z14test_sig_abortv
00000000000013b0 T _Z19test_sig_abort_freev
000000000000138d T _Z21test_sig_abort_assertv
0000000000001330 T _Z6myfunci
0000000000001330 t _Z6myfunci.localalias.0
00000000000011f5 T _Z7myfunc3v
0000000000001324 t _ZL7myfunc2v
00000000000020c0 r _ZZ21test_sig_abort_assertvE19__PRETTY_FUNCTION__
可以看到里面函数名都是mangle后的符号,可以用c++filt demangle一下
bash$ c++filt _Z6myfunci
myfunc(int)
下面我们把Makefile里的
LINKFLAGS += -rdynamic
再解屏,看看效果
bash$ ./main
pid 15697
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(_Z7myfunc3v+0x2e) [0x55e95c4a0223]
./main(_Z14dump_backtracei+0x35) [0x55e95c4a0321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f46e4fff100]
./main(_Z13test_sig_segvv+0x14) [0x55e95c4a036c]
./main(main+0x6e) [0x55e95c4a04ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f46e4fe209b]
./main(_start+0x2a) [0x55e95c4a013a]
Segmentation fault (core dumped)
可以看到生成的不是地址了,而是./main(函数符号+偏移地址),那这样我们怎么求出崩溃地址呢?
这里就需要用到上面小知识里说到的nm了,我们可以从nm里查到相应符号的地址,再加上偏移地址,就可以得到崩溃时候的地址了.
# 假如我们要求下面_Z7myfunc3v+0x2e的地址
./main(_Z7myfunc3v+0x2e) [0x55e95c4a0223]
bash$ nm main | grep _Z7myfunc3v
00000000000011f5 T _Z7myfunc3v
#查到地址是00000000000011f5, 然后再用0x11f5 + 0x2e就可以了
#先把16进制转为10进制
bash$ printf "%d %d\n" 0x11f5 0x2e
4597 46
#再expr求一下
bash$ expr 4597 + 46
4643
#再转为16进制
bash$ printf "%x\n" 4643
1223
#再addr2line看一下就可以得到行号了
bash$ addr2line -e main 0x1223 -sfC
myfunc3()
main.cpp:14
接着我们再做几个测试,之前我们用的是debug, -g编译参数,下面我们分别做release和不带-g的测试
看看崩溃后是否还能依据地址找到代码位置
# 先测试-DDEBUG 不带 -g
# Makefile里用这句
FLAGS = -DDEBUG -W -Wall -fPIC
# 为了能直接得到地址,咱们把-rdynamic去掉,这样就不用再求地址了
#LINKFLAGS += -rdynamic
运行一下程序
bash$ ./main
pid 19316
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x560986db5223]
./main(+0x1321) [0x560986db5321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7fc880e69100]
./main(+0x136c) [0x560986db536c]
./main(+0x14ce) [0x560986db54ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fc880e4c09b]
./main(+0x113a) [0x560986db513a]
Segmentation fault (core dumped)
addr2line看一下,结论,debug模式下,去掉-g看不到行号了,因为没有调试信息了
bash$ addr2line -e main 0x1223 -sfC
myfunc3()
??:?
再来用release, -g测试一下
#Makefile里用这句
FLAGS = -g -DNDEBUG -W -Wall -fPIC
运行一下,让程序崩一个,注意,代码没变, release和debug崩的地址变了,debug是0x1223,release是0x1213
bash$ ./main
pid 19340
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1213) [0x5602bc813213]
./main(+0x1311) [0x5602bc813311]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7fd0aedd5100]
./main(+0x135c) [0x5602bc81335c]
./main(+0x14a2) [0x5602bc8134a2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fd0aedb809b]
./main(+0x112a) [0x5602bc81312a]
Segmentation fault (core dumped)
再来addr2line看一下,结论: release下,带-g可以看到行号和函数符号
bash$ addr2line -e main 0x1213 -sfC
myfunc3()
main.cpp:14
再测试最后一种情况,release不带-g
#Makefile里用这句,其他的FLAGS屏掉
FLAGS = -DNDEBUG -W -Wall -fPIC
编完,跑一下程序
bash$ ./main
pid 19367
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1213) [0x55801b953213]
./main(+0x1311) [0x55801b953311]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f823c12b100]
./main(+0x135c) [0x55801b95335c]
./main(+0x14a2) [0x55801b9534a2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f823c10e09b]
./main(+0x112a) [0x55801b95312a]
Segmentation fault (core dumped)
addr2line看一下,结论,release不带-g看不到行号,依然能看到函数符号
bash$ addr2line -e main 0x1213 -sfC
myfunc3()
??:?
总结一下:
- 不管是release还是debug,都能看到函数符号
- 带-g能看到行号
- 不带-g看不到行号
所以线上时候我们一般会打个release的程序,但是带-g,便于定位
使用dmesg求出崩溃所在的位置
最后这个办法是linux自带的,当程序崩溃后,系统会记个log,当我们没有core,也没有自己打印backtrace,或者由于一些原因,core或log被删掉了,总之程序崩了之后什么线索都没有的时候,就可以用dmesg定位了.
我们这次用debug, -g先编好程序,跑一下
bash$ ./main
pid 19429
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x55fa146b3223]
./main(+0x1321) [0x55fa146b3321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f5faa0ee100]
./main(+0x136c) [0x55fa146b336c]
./main(+0x14ce) [0x55fa146b34ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f5faa0d109b]
./main(+0x113a) [0x55fa146b313a]
Segmentation fault (core dumped)
再用dmesg查一下崩溃log,因为dmesg会输出一大堆东西,我们过滤一下,只看带main的,因为我们程序名叫main
bash$ dmesg | grep main
... # 前面的略
[159708.390262] traps: main[12287] trap divide error ip:55644504443b sp:7fffc2a9c230 error:0 in main[556445044000+1000]
... # 中间的略
[239814.618107] main[19429]: segfault at 0 ip 000055fa146b336c sp 00007ffd4b2b4180 error 4 in main[55fa146b3000+1000]
上面我截了两行内容,dmesg最下面的是最后一次发生崩溃的记录,说一下输出的内容
[系统开启的秒数] 程序名[pid]: 错误原因 ip 指令地址 sp 栈顶地址 错误号 main[基址+大小]
[239814.618107] main[19429]: segfault at 0 ip 000055fa146b336c sp 00007ffd4b2b4180 error 4 in main[55fa146b3000+1000]
下面的错误原因分别有trap divide error,表示除0的错,另一个segfault at 0是空指针引起的段错误, 后面的error号也不一样.
拿到这个log的时候,我们看到最后一条记录main[19429],pid正好与最后一次我们运行程序时候打印的pid相同,表示是同一次崩溃了.
下面我们开始算术,求出崩溃地址.
我们需要关注的是 ip 000055fa146b336c 和 main[55fa146b3000+1000] 这两列,ip指的是指令在崩溃的时候指向的地址, main后面是程序的基址,所以我们可以想到ip - bp 就是偏移地址(bp是基址的意思),也就是程序崩溃时候的地址了,下面我们验证一下
# 先把ip, bp转为10进制,方便expr运算
bash$ printf "%d %d %d\n" 0x000055fa146b336c 0x55fa146b3000 0x1000
94532572754796 94532572753920 4096
# 再epxr算一下 addr = ip - bp
bash$ expr 94532572754796 - 94532572753920 + 4096
4972
# 再把4972转为16进制
printf "%x\n" 4972
136c
下面我们拿addr2line查一下这个地址的行号,定位了
bash$ addr2line -e main 0x136c -sfC
test_sig_segv()
main.cpp:54
下面就是引起崩溃的位置了,确实是54行
51 void test_sig_segv(){
52 //这个就是段错误了
53 int* p = NULL;
54 printf("*p = %d\n", *p);
55 }