查看原文
其他

揭秘go内存!

邵珠光 腾讯云开发者 2022-12-22


导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后台开发工程师邵珠光。

在处理内存泄露的时候,想到了一种从内存中查看哪些对象的问题,于是就对实际跑着的程序内存进行了解析,通过可视化的方式有助于理解go的内存布局和管理。



基础知识


在本篇文章开始前,希望你可以了解go的一些基本的内存知识,不需要太深入,简单总结了如下几点:

(一)内存布局


内存布局包括内存对齐,一个结构体占用内存大小等。另外,对于go语言而言,其内存中的堆对象中本身并没有含有该对象的任何标识信息,例如类型等。在go语言中属性的布局与代码顺序有关,不会进行自动调整。



(二)常见类型


对于字符串类型,实际上它是一个具有两个属性的结构:


type StringHeader struct { Data uintptr Len int}

对于数组而言,它有三个属性:


type SliceHeader struct { Data uintptr Len int Cap int}


也就是说,如果我们看到一个结构体中含有字符串,那么这个字符串占多少个字节呢?对于64位系统而言就是16个字节,8个表示地址,另外8个表示长度。



测试代码


(一)定义两个结构体


首先我们定义两个结构体:


type User struct { Name string Age uint8 Sex uint8 class *Class}
type Class struct { CName string Index uint}


其中一个结构体包含了另外一个结构体,下面我们来看下这两个结构体的布局格式(在64位系统中)。



(二)Class的内存布局


Class结构中只有两个属性,一个是字符串,另外一个是uint,对于后者而言在64位系统中就是uint64,则它的结构包括了24个字节:


cl := new(Class)fmt.Println(unsafe.Sizeof(*cl))fmt.Println(unsafe.Alignof(*cl))// 输出为:// 24// 8// 在内存中的结构应该如下:|0 - 7|8 - 15|16 - 23||0 - 7|:CName的指针|8 - 15|:CName的长度|16 - 23|:Index



(三)User的内存布局


User结构比较复杂,对于引用的Class而言,这就是一个指针而已,指针就是8字节,那么它的整体结构应该是占用了32个字节(特别关注uint8,该值只占用一个字节),结构如下:


u := new(User)fmt.Println(unsafe.Sizeof(*u))fmt.Println(unsafe.Alignof(*u))// 输出结果为:// 32// 8|0 - 7|8 - 15|16|17|18 - 23|24 - 31||0 - 7|:Name的指针|8 - 15|:Name的长度|16|:Age|17|:Sex|18 - 23|:什么都没有,浪费了|24 - 31|:class,即指针



(四)测试代码


我们写了非常少的一段代码,来测试下这两个对象的内存布局:


/* Copyright (C) THL A29 Limited, a Tencent company. All rights reserved. SPDX-License-Identifier: Apache-2.0*/package main
import ( "fmt" "math/rand" "os" "os/signal" "strconv")
var user *User
func main() { idx := rand.Intn(10) user = &User{ Name: "zhangsan", Age: 18, Sex: 1, class: &Class{ CName: "class-" + strconv.Itoa(idx), Index: uint(idx), }, } fmt.Println(user) c := make(chan os.Signal) signal.Notify(c, os.Interrupt, os.Kill) s := <-c fmt.Println("receive signal -> ", s)}
type User struct { Name string Age uint8 Sex uint8 class *Class}
type Class struct { CName string Index uint}


其中的字符串,我特意使用了两个不同的方式,一个是独立的字符串,或者说字符串常量:"zhangsan",另外一个是字符串的拼接。


这两者是有区别的,对于常量字符串而言,通常会在编译期间将其放在程序段中,而拼接字符串是在运行期生成的,那么我们是可以看到它是明确在堆上生成的。



解析


(一)运行起来看内存


首先把程序跑起来,其PID为14173,我们如何查看内存分配呢?最简单的方式就是直接通过/proc/14173/maps来查看:


[root@VM-0-16-centos /home/leon]# cat /proc/14173/maps00400000-00482000 r-xp 00000000 fd:01 672257 /root/chainmaker/my-mem/my-mem00482000-00510000 r--p 00082000 fd:01 672257 /root/chainmaker/my-mem/my-mem00510000-0052a000 rw-p 00110000 fd:01 672257 /root/chainmaker/my-mem/my-mem0052a000-0055e000 rw-p 00000000 00:00 0 c000000000-c000400000 rw-p 00000000 00:00 0 c000400000-c004000000 ---p 00000000 00:00 0 7fc393f86000-7fc3962f7000 rw-p 00000000 00:00 0 7fc3962f7000-7fc3a6477000 ---p 00000000 00:00 0 7fc3a6477000-7fc3a6478000 rw-p 00000000 00:00 0 7fc3a6478000-7fc3b8327000 ---p 00000000 00:00 0 7fc3b8327000-7fc3b8328000 rw-p 00000000 00:00 0 7fc3b8328000-7fc3ba6fd000 ---p 00000000 00:00 0 7fc3ba6fd000-7fc3ba6fe000 rw-p 00000000 00:00 0 7fc3ba6fe000-7fc3bab77000 ---p 00000000 00:00 0 7fc3bab77000-7fc3bab78000 rw-p 00000000 00:00 0 7fc3bab78000-7fc3babf7000 ---p 00000000 00:00 0 7fc3babf7000-7fc3bac57000 rw-p 00000000 00:00 0 7fff7aad6000-7fff7aaf7000 rw-p 00000000 00:00 0 [stack]7fff7ab9a000-7fff7ab9d000 r--p 00000000 00:00 0 [vvar]7fff7ab9d000-7fff7ab9e000 r-xp 00000000 00:00 0 [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]


上面的信息中可以看出一部分端倪来,对于前三行,理论上应该是程序段,中间的一些信息是堆的数据,最后应该是系统调用。



(二)把内存中的数据dump下来


可以使用gdb命令,直接将内存中的数据dump为二进制文件,为此编写了一个脚本,可以直接执行:


#!/bin/bash
## 注意过滤条件,也可以添加其他过滤条件# 为了全部处理下来,可以将第一行修改为:cat /proc/$1/maps \grep rw-p /proc/$1/maps \| sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' \| while read start stop; do \ gdb --batch --pid $1 -ex \ "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; \done


它的入参是进程ID,调用后,会将当时内存中的信息dump成文件:


[root@VM-0-16-centos /home/leon]# ./dump-all-mem.sh 14173runtime.futex () at /usr/local/go/src/runtime/sys_linux_amd64.s:553553 MOVL AX, ret+40(FP)......warning: File "/usr/local/go/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load [Inferior 1 (process 14173) detached][root@VM-0-16-centos /home/leon]# ll -h-rw-r--r-- 1 root root 532480 Aug 1 10:44 14173-00400000-00482000.dump-rw-r--r-- 1 root root 581632 Aug 1 10:44 14173-00482000-00510000.dump-rw-r--r-- 1 root root 106496 Aug 1 10:44 14173-00510000-0052a000.dump-rw-r--r-- 1 root root 212992 Aug 1 10:44 14173-0052a000-0055e000.dump-rw-r--r-- 1 root root 37163008 Aug 1 10:44 14173-7fc393f86000-7fc3962f7000.dump-rw-r--r-- 1 root root 270008320 Aug 1 10:44 14173-7fc3962f7000-7fc3a6477000.dump-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3a6477000-7fc3a6478000.dump-rw-r--r-- 1 root root 300609536 Aug 1 10:44 14173-7fc3a6478000-7fc3b8327000.dump-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3b8327000-7fc3b8328000.dump-rw-r--r-- 1 root root 37572608 Aug 1 10:44 14173-7fc3b8328000-7fc3ba6fd000.dump-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3ba6fd000-7fc3ba6fe000.dump-rw-r--r-- 1 root root 4689920 Aug 1 10:44 14173-7fc3ba6fe000-7fc3bab77000.dump-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3bab77000-7fc3bab78000.dump-rw-r--r-- 1 root root 520192 Aug 1 10:44 14173-7fc3bab78000-7fc3babf7000.dump-rw-r--r-- 1 root root 393216 Aug 1 10:44 14173-7fc3babf7000-7fc3bac57000.dump-rw-r--r-- 1 root root 135168 Aug 1 10:44 14173-7fff7aad6000-7fff7aaf7000.dump-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fff7ab9d000-7fff7ab9e000.dump-rw-r--r-- 1 root root 4194304 Aug 1 10:44 14173-c000000000-c000400000.dump-rw-r--r-- 1 root root 62914560 Aug 1 10:44 14173-c000400000-c004000000.dump


其中以进程ID开头的dump文件就是我们dump下来的内存数据。



(三)从dump文件中查找class-


我们定义了一个特别的字符串用于过滤,那就是class-,因为后面的值是随机的,我们不清楚是什么,但通过这个字符串足够。


此时使用strings命令来查找,该命令会将能转为字符串的转为字符串查看,我们的目的是找到在具体哪个文件中:


[root@VM-0-16-centos /home/leon]# strings 14173-c000000000-c000400000.dump | grep class-class-1


最终我们找到了这个dump文件:14173-c000000000-c000400000.dump



(四)仔细看看这个dump文件


下面我们仔细看看这个dump文件里面是什么,因为其中的内容是二进制,所以我们采用十六进制的方式来查看,使用的命令是hexdump,下面是内容:


[root@VM-0-16-centos /home/leon]# hexdump -c 14173-c000000000-c000400000.dump### 解释下下面的内容,否则可能无法理解,以第一行为例### 每一行分为两部分,前面第一段表示的是偏移量,后面是内容,内容是十六个字节,详细说明如下:## 0000000:偏移量,是基于初始内存地址的,此处是c000000000,这个非常重要,是我们找地址的本质所在,真实的地址是将这个偏移量与初始地址相加,例如00000f0对应的地址为:c0000000f0## 剩下的就是十六个字节的内容,对于里面的显示需要进行一些说明:# \0:表示0# 312:这种以3个数字描述的是其实是8进制,其中最高位占2bit,剩下两个各占3bit,312=11001010(二进制)=0xca(十六进制)# 2:这个数字2不是数字2,如果是2会以002的方式描述,这个数字2是一个ascii码,它实际代表的值是:50(十进制)或0x32(十六进制)# \a:还有类似的,如\t \b等和上面的2是一样的,都是ascii码### 另外需要说明的是,如果该行出现了*,表示不是一段连续的内存地址,类似于分割符0000000 027 221 I \0 \0 \0 \0 \0 003 \0 \0 \0 \0 \0 \0 \00000010 001 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000020 032 221 I \0 \0 \0 \0 \0 003 \0 \0 \0 \0 \0 \0 \00000030 \0 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000040 035 221 I \0 \0 \0 \0 \0 003 \0 \0 \0 \0 \0 \0 \00000050 002 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000060 312 221 I \0 \0 \0 \0 \0 004 \0 \0 \0 \0 \0 \0 \00000070 003 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000080 322 221 I \0 \0 \0 \0 \0 004 \0 \0 \0 \0 \0 \0 \00000090 004 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000000a0 326 221 I \0 \0 \0 \0 \0 004 \0 \0 \0 \0 \0 \0 \000000b0 005 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000000c0 002 222 I \0 \0 \0 \0 \0 004 \0 \0 \0 \0 \0 \0 \000000d0 006 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000000e0 & 221 I \0 \0 \0 \0 \0 003 \0 \0 \0 \0 \0 \0 \000000f0 \a 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000100 235 233 I \0 \0 \0 \0 \0 \t \0 \0 \0 \0 \0 \0 \00000110 \t 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000120 330 224 I \0 \0 \0 \0 \0 006 \0 \0 \0 \0 \0 \0 \00000130 \n 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000140 336 224 I \0 \0 \0 \0 \0 006 \0 \0 \0 \0 \0 \0 \00000150 \v 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000160 2 222 I \0 \0 \0 \0 \0 004 \0 \0 \0 \0 \0 \0 \00000170 \f 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00000180 = 223 I \0 \0 \0 \0 \0 005 \0 \0 \0 \0 \0 \0 \00000190 016 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000001a0 B 223 I \0 \0 \0 \0 \0 005 \0 \0 \0 \0 \0 \0 \000001b0 017 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000001c0 G 223 I \0 \0 \0 \0 \0 005 \0 \0 \0 \0 \0 \0 \000001d0 \r 216 U \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000001e0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0* // 将上面和下面的内存分割开了0002000 \0 @ \0 \0 300 \0 \0 \0 \0 300 \0 \0 300 \0 \0 \00002010 240 C \0 \0 300 \0 \0 \0 240 C \0 \0 300 \0 \0 \0


我们的目的是找到class-,所以我们只需要看它前后的一部分内容即可:


[root@VM-0-16-centos /home/leon]# hexdump -c 14173-c000000000-c000400000.dump | grep -A 3 -B 3 "c l" 00ab300 i 254 321 332 6 Y 315 246 \0 \0 \0 \0 \0 \0 \0 \000ab310 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0*00ae010 c l a s s - 1 \0 & { \0 \0 \0 \0 \0 \000ae020 & { z h a n g s a n 1 8 1 00ae030 022 001 \0 \0 004 002 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000ae040 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0


可以看出,class-1这个字符串所在的地址是:00ae010+c000000000=c0000ae010,根据理论,下面就是要找到哪引用了这个地址。



(五)找到引用地址的位置


在找到引用地址c0000ae010之前,我们首先需要做一个计算,因为这是一个十六进制,我们知道实际上通过hexdump看的时候里面大部分都是八进制,计算很简单,将地址以两两进行分开即可:c0 00 0a e0 10,经过计算可以得出如下结果:


300 0 12 340 20,我们需要将该结果返回来,容易进行查找,那么需要查找的结果可能是:20 340 012 \0 300。(实际上不是)。为什么不是?需要做一个说明:


标准的ascii码表示的是从0~127,这个值如果以八进制表示的话就是从:0~177;对于这个范围的值都会以ascii码的形式展示,对于12而言,其ascii码的显示是换行符,也就是\n;对于20而言,它的ascii码显示是:数据链路转义,这个没有可显示的字符对应,通常会使用原始的020来描述。


那么我们要查询的结果应该就是:020 340 \n \0 300


通过我们的搜索还真的找到了该引用:


### 为了查找方便,首先我把所有的dump文件全部转为hex输出到了指定的文件(后缀为.hex的),列表:[root@VM-0-16-centos /home/leon]# ll-rw-r--r-- 1 root root 532480 Aug 1 10:44 14173-00400000-00482000.dump-rw-r--r-- 1 root root 2379546 Aug 1 10:58 14173-00400000-00482000.dump.hex-rw-r--r-- 1 root root 581632 Aug 1 10:44 14173-00482000-00510000.dump-rw-r--r-- 1 root root 2595452 Aug 1 10:58 14173-00482000-00510000.dump.hex-rw-r--r-- 1 root root 106496 Aug 1 10:44 14173-00510000-0052a000.dump-rw-r--r-- 1 root root 452260 Aug 1 10:59 14173-00510000-0052a000.dump.hex-rw-r--r-- 1 root root 212992 Aug 1 10:44 14173-0052a000-0055e000.dump-rw-r--r-- 1 root root 42992 Aug 1 10:59 14173-0052a000-0055e000.dump.hex-rw-r--r-- 1 root root 37163008 Aug 1 10:44 14173-7fc393f86000-7fc3962f7000.dump-rw-r--r-- 1 root root 34282 Aug 1 10:59 14173-7fc393f86000-7fc3962f7000.dump.hex-rw-r--r-- 1 root root 270008320 Aug 1 10:44 14173-7fc3962f7000-7fc3a6477000.dump-rw-r--r-- 1 root root 83 Aug 1 10:59 14173-7fc3962f7000-7fc3a6477000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3a6477000-7fc3a6478000.dump-rw-r--r-- 1 root root 154 Aug 1 11:00 14173-7fc3a6477000-7fc3a6478000.dump.hex-rw-r--r-- 1 root root 300609536 Aug 1 10:44 14173-7fc3a6478000-7fc3b8327000.dump-rw-r--r-- 1 root root 83 Aug 1 11:00 14173-7fc3a6478000-7fc3b8327000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3b8327000-7fc3b8328000.dump-rw-r--r-- 1 root root 154 Aug 1 11:00 14173-7fc3b8327000-7fc3b8328000.dump.hex-rw-r--r-- 1 root root 37572608 Aug 1 10:44 14173-7fc3b8328000-7fc3ba6fd000.dump-rw-r--r-- 1 root root 82 Aug 1 11:01 14173-7fc3b8328000-7fc3ba6fd000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3ba6fd000-7fc3ba6fe000.dump-rw-r--r-- 1 root root 154 Aug 1 11:01 14173-7fc3ba6fd000-7fc3ba6fe000.dump.hex-rw-r--r-- 1 root root 4689920 Aug 1 10:44 14173-7fc3ba6fe000-7fc3bab77000.dump-rw-r--r-- 1 root root 82 Aug 1 11:01 14173-7fc3ba6fe000-7fc3bab77000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fc3bab77000-7fc3bab78000.dump-rw-r--r-- 1 root root 228 Aug 1 11:02 14173-7fc3bab77000-7fc3bab78000.dump.hex-rw-r--r-- 1 root root 520192 Aug 1 10:44 14173-7fc3bab78000-7fc3babf7000.dump-rw-r--r-- 1 root root 82 Aug 1 11:02 14173-7fc3bab78000-7fc3babf7000.dump.hex-rw-r--r-- 1 root root 393216 Aug 1 10:44 14173-7fc3babf7000-7fc3bac57000.dump-rw-r--r-- 1 root root 37582 Aug 1 11:03 14173-7fc3babf7000-7fc3bac57000.dump.hex-rw-r--r-- 1 root root 135168 Aug 1 10:44 14173-7fff7aad6000-7fff7aaf7000.dump-rw-r--r-- 1 root root 24072 Aug 1 11:04 14173-7fff7aad6000-7fff7aaf7000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-7fff7ab9d000-7fff7ab9e000.dump-rw-r--r-- 1 root root 17510 Aug 1 11:02 14173-7fff7ab9d000-7fff7ab9e000.dump.hex-rw-r--r-- 1 root root 4194304 Aug 1 10:44 14173-c000000000-c000400000.dump-rw-r--r-- 1 root root 195662 Aug 1 11:02 14173-c000000000-c000400000.dump.hex-rw-r--r-- 1 root root 62914560 Aug 1 10:44 14173-c000400000-c004000000.dump-rw-r--r-- 1 root root 82 Aug 1 11:03 14173-c000400000-c004000000.dump.hex-rw-r--r-- 1 root root 4096 Aug 1 10:44 14173-ffffffffff600000-ffffffffff601000.dump-rw-r--r-- 1 root root 446 Aug 1 11:03 14173-ffffffffff600000-ffffffffff601000.dump.hex### 然后通过grep命令可以找到对应行:[root@VM-0-16-centos /home/leon]# grep "020 340 \\\\n \\\\0 300" 14173-*.hex14173-c000000000-c000400000.dump.hex:009c010 p 374 \t \0 300 \0 \0 \0 020 340 \n \0 300 \0 \0 \0



(六)Class对象分析


我们将查找范围稍微扩大几行(上下均扩大了几行):


## grep命令中-A表示向后(after),-B表示向前(before)[root@VM-0-16-centos /home/leon]# grep "020 340 \\\\n \\\\0 300" -A 3 -B 3 14173-*.hex14173-c000000000-c000400000.dump.hex-009af30 300 @ R \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \014173-c000000000-c000400000.dump.hex-009af40 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \014173-c000000000-c000400000.dump.hex-*14173-c000000000-c000400000.dump.hex:009c010 p 374 \t \0 300 \0 \0 \0 020 340 \n \0 300 \0 \0 \014173-c000000000-c000400000.dump.hex-009c020 \a \0 \0 \0 \0 \0 \0 \0 001 \0 \0 \0 \0 \0 \0 \014173-c000000000-c000400000.dump.hex-009c030 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \014173-c000000000-c000400000.dump.hex-*


我们根据代码来分析一下这段内存信息,首先将之前class的内存布局拿过来:


// 在内存中的结构应该如下:|0 - 7|8 - 15|16 - 23||0 - 7|:CName的指针|8 - 15|:CName的长度|16 - 23|:Index


下面是根据内存布局的分析结果:


####### |-- 此处为其他内容,不用关注 ------|----------CName指针-----------|009c010 p 374 \t \0 300 \0 \0 \0 020 340 \n \0 300 \0 \0 \0####### |CName长度为\a,一个转义符,表示7, |---Index的值为001,也就是1,符合|009c020 \a \0 \0 \0 \0 \0 \0 \0 001 \0 \0 \0 \0 \0 \0 \0


其中的009c018即为该地址的偏移地址(注意最后一位是8,因为没有正好在009c010开头),下面我们要找的就是User这个对象。



(七)User对象查找


还是按照相同的方式将地址c00009c018(009c018 + c000000000)找出来。


先换算,过程不再写,换算后的结果为:030 300 \t \0 300


然后进行查找,发现了4个:


### 注意grep查找\的时候需要\\\\来转义[root@VM-0-16-centos /home/leon]# grep "030 300 \\\\t \\\\0 300" 14173-*.dump.hex14173-c000000000-c000400000.dump.hex:0088f00 030 300 \t \0 300 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \014173-c000000000-c000400000.dump.hex:00b0010 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \014173-c000000000-c000400000.dump.hex:00b0030 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \014173-c000000000-c000400000.dump.hex:00bbf00 030 300 \t \0 300 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \0


我们对这四个挨着进行分析,他们都在文件14173-c000000000-c000400000.dump.hex中。


[root@VM-0-16-centos /home/leon]# grep "030 300 \\\\t \\\\0 300" -A 3 -B 3 14173-c000000000-c000400000.dump.hex0088ed0 001 \0 \0 \0 \0 \0 \0 \0 001 \0 \0 \0 \0 \0 \0 \0*0088ef0 v 356 I \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \00088f00 030 300 \t \0 300 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \00088f10 300 h H \0 \0 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \00088f20 \0 \0 \0 \0 \0 \0 \0 \0 X P @ \0 \0 \0 \0 \00088f30 p 217 \b \0 300 \0 \0 \0 231 O @ \0 \0 \0 \0 \0--00ae040 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0*00b0000 ^ 231 I \0 \0 \0 \0 \0 \b \0 \0 \0 \0 \0 \0 \000b0010 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \000b0020 ^ 231 I \0 \0 \0 \0 \0 \b \0 \0 \0 \0 \0 \0 \000b0030 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \000b0040 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0*00b2080 340 _ I \0 \0 \0 \0 \0 0 \f \t \0 300 \0 \0 \0--00bbed0 001 \0 \0 \0 \0 \0 \0 \0 001 \0 \0 \0 \0 \0 \0 \0*00bbef0 v 356 I \0 \0 \0 \0 \0 300 \b \0 300 \0 \0 \000bbf00 030 300 \t \0 300 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \000bbf10 300 h H \0 \0 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \000bbf20 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \000bbf30 p 217 \b \0 300 \0 \0 \0 231 O @ \0 \0 \0 \0 \0


结合User的内存布局(如下),在class前面应该有24个字节,表示的是开头:


## |0 - 7|8 - 15|16|17|18 - 23|24 - 31|## |0 - 7|:Name的指针## |8 - 15|:Name的长度## |16|:Age## |17|:Sex## |18 - 23|:什么都没有,浪费了## |24 - 31|:class,即指针


首先分析第一个:


0056010 ` + \0 \0 300 \0 \0 \0 \0 - \0 \0 300 \0 \0 \0###### |----- | 此处应该是Age+Sex,为022 001,不合法|0088ef0 v 356 I \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0###### |----- class指针 -----|0088f00 030 300 \t \0 300 \0 \0 \0 \0 \0 \v \0 300 \0 \0 \0


可以看出来,该地址并不是User的地址信息,因为映射Age和Sex的位置不合法。


通过分析我们知道了,Age和Sex的位置应该是022和001,那么可以直接进行过滤,就剩下了两个地址:


#######| ----- "zhangsan"所在地址 ----- |-------- 字符串长度为:8 -------|00b0000 ^ 231 I \0 \0 \0 \0 \0 \b \0 \0 \0 \0 \0 \0 \0#######|Age|Sex| |------- class ----------|00b0010 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \0#######| ----- "zhangsan"所在地址 ----- |-------- 字符串长度为:8 -------|00b0020 ^ 231 I \0 \0 \0 \0 \0 \b \0 \0 \0 \0 \0 \0 \0#######|Age|Sex| |------- class ----------|00b0030 022 001 \0 \0 \0 \0 \0 \0 030 300 \t \0 300 \0 \0 \000b0040 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0

如上面标注的一样,这两个地址都是合法的。简单说明一下:


  • 在程序中,我们设置了Age=18,此处的022(八进制)=2 * 8 + 2 = 18,是符合预期的;


  • 在程序中设置的Sex=1与内存中的001也是对应成功的;


  • 在程序中,我们设置的Name为"zhangsan",也就是8个字节,\b在ascii码中表示退格键,正好是十进制的8,八进制的10,也是符合预期的。


最后看一下Name指针,这是一个比较特殊的部分,它的地址内容是:^ 231 I,换算成十六进制为5e 99 49,对应的绝对地址是:49995e。


(八)查找User中的zhangsan


可以看到,绝对地址为49995e的内存在:14173-00482000-00510000.dump中,进行偏移量的计算:


偏移量=49995e-482000=1795e,但需要考虑的是,该偏移量并不是一个可以被十六进制整除的值,也就是它不会出现在文件的最开始一列,它对应的开头的地址应该是17950。


下面我们可以所有17950,可以看到如下的信息:


[root@VM-0-16-centos /home/leon]# grep "17950" -A 1 14173-00482000-00510000.dump.hex0017950 a c e B u f u n k n o w n ( z h0017960 a n g s a n ( f o r c e d )

可以很明确的看到,zhangsan的地址确实是1795e地址。这个字符串具体在哪呢?



(九)zhangsan在哪


我们重新回归到目录:/proc/14173/maps,它的前三行:


[root@VM-0-16-centos /home/leon]# cat /proc/14173/maps00400000-00482000 r-xp 00000000 fd:01 672257 /root/chainmaker/my-mem/my-mem00482000-00510000 r--p 00082000 fd:01 672257 /root/chainmaker/my-mem/my-mem00510000-0052a000 rw-p 00110000 fd:01 672257 /root/chainmaker/my-mem/my-mem


zhangsan的内容恰恰位于第2行,注意第二行的权限标识:r--p,该权限标识它是一个只读的,不可以执行,什么数据是只读的,不可执行的,一般来讲就是放入的常量池。另外,需要看到的是最开始的三行都是描述的当前进程的信息。简单的说明如下:


[root@VM-0-16-centos /home/leon]# cat /proc/14173/maps### 可读可执行,但不可写,通常是代码段的位置00400000-00482000 r-xp 00000000 fd:01 672257 /root/chainmaker/my-mem/my-mem### 只读,不可执行不可写,一般放的是常量池,就是那些不会修改的常量,在go语言中常见的一般是字符串00482000-00510000 r--p 00082000 fd:01 672257 /root/chainmaker/my-mem/my-mem### 可读,可写,但不可执行,一般会放全局变量,该类值是可以被修改的00510000-0052a000 rw-p 00110000 fd:01 672257 /root/chainmaker/my-mem/my-mem

参考资料:
1.进程内存sysfs解读:
https://www.cnblogs.com/arnoldlu/p/8568330.html
2./proc/<pid>/maps简要分析:
https://www.cnblogs.com/arnoldlu/p/10272466.html


 作者简介


邵珠光

腾讯后台开发工程师

腾讯后台开发工程师,目前主要负责区块链开源底层平台-长安链的设计与研发工作,工作中喜欢总结与思考,对技术有独特的热爱。



 推荐阅读


C++20协程初探!
GooseFS 在云端数据湖存储上的降本增效实践
新周期重构地产与物业数智化价值,TVP行业大使有话说
轻松上手!手把手带你掌握从Context到go设计理念


👇点击「阅读原文」注册成为社区创作者,认识大咖,打造你的技术影响力!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存