揭祕 go 內存!

本文推選自騰訊雲開發者社區 -【技思廣益 · 騰訊技術人原創集】專欄。該專欄是騰訊雲開發者社區爲騰訊技術人與廣泛開發者打造的分享交流窗口。欄目邀約騰訊技術人分享原創的技術積澱,與廣泛開發者互啓迪共成長。本文作者是騰訊後臺開發工程師邵珠光。

在處理內存泄露的時候,想到了一種從內存中查看哪些對象的問題,於是就對實際跑着的程序內存進行了解析,通過可視化的方式有助於理解 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/maps
00400000-00482000 r-xp 00000000 fd:01 672257                             /root/chainmaker/my-mem/my-mem
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
0052a000-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 14173
runtime.futex () at /usr/local/go/src/runtime/sys_linux_amd64.s:553
553             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  \0
0000010 001 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000020 032 221   I  \0  \0  \0  \0  \0 003  \0  \0  \0  \0  \0  \0  \0
0000030  \0 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000040 035 221   I  \0  \0  \0  \0  \0 003  \0  \0  \0  \0  \0  \0  \0
0000050 002 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000060 312 221   I  \0  \0  \0  \0  \0 004  \0  \0  \0  \0  \0  \0  \0
0000070 003 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000080 322 221   I  \0  \0  \0  \0  \0 004  \0  \0  \0  \0  \0  \0  \0
0000090 004 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00000a0 326 221   I  \0  \0  \0  \0  \0 004  \0  \0  \0  \0  \0  \0  \0
00000b0 005 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00000c0 002 222   I  \0  \0  \0  \0  \0 004  \0  \0  \0  \0  \0  \0  \0
00000d0 006 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00000e0   & 221   I  \0  \0  \0  \0  \0 003  \0  \0  \0  \0  \0  \0  \0
00000f0  \a 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000100 235 233   I  \0  \0  \0  \0  \0  \t  \0  \0  \0  \0  \0  \0  \0
0000110  \t 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000120 330 224   I  \0  \0  \0  \0  \0 006  \0  \0  \0  \0  \0  \0  \0
0000130  \n 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000140 336 224   I  \0  \0  \0  \0  \0 006  \0  \0  \0  \0  \0  \0  \0
0000150  \v 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000160   2 222   I  \0  \0  \0  \0  \0 004  \0  \0  \0  \0  \0  \0  \0
0000170  \f 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000180   = 223   I  \0  \0  \0  \0  \0 005  \0  \0  \0  \0  \0  \0  \0
0000190 016 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00001a0   B 223   I  \0  \0  \0  \0  \0 005  \0  \0  \0  \0  \0  \0  \0
00001b0 017 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00001c0   G 223   I  \0  \0  \0  \0  \0 005  \0  \0  \0  \0  \0  \0  \0
00001d0  \r 216   U  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00001e0  \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  \0
0002010 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  \0
00ab310  \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  \0
00ae020   &   {   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  \0
00ae040  \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-*.hex
14173-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-*.hex
14173-c000000000-c000400000.dump.hex-009af30 300   @   R  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
14173-c000000000-c000400000.dump.hex-009af40  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
14173-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  \0
14173-c000000000-c000400000.dump.hex-009c020  \a  \0  \0  \0  \0  \0  \0  \0 001  \0  \0  \0  \0  \0  \0  \0
14173-c000000000-c000400000.dump.hex-009c030  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
14173-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.hex
14173-c000000000-c000400000.dump.hex:0088f00 030 300  \t  \0 300  \0  \0  \0  \0  \0  \v  \0 300  \0  \0  \0
14173-c000000000-c000400000.dump.hex:00b0010 022 001  \0  \0  \0  \0  \0  \0 030 300  \t  \0 300  \0  \0  \0
14173-c000000000-c000400000.dump.hex:00b0030 022 001  \0  \0  \0  \0  \0  \0 030 300  \t  \0 300  \0  \0  \0
14173-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.hex
0088ed0 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  \0
0088f00 030 300  \t  \0 300  \0  \0  \0  \0  \0  \v  \0 300  \0  \0  \0
0088f10 300   h   H  \0  \0  \0  \0  \0  \0  \0  \v  \0 300  \0  \0  \0
0088f20  \0  \0  \0  \0  \0  \0  \0  \0   X   P   @  \0  \0  \0  \0  \0
0088f30   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  \0
00b0010 022 001  \0  \0  \0  \0  \0  \0 030 300  \t  \0 300  \0  \0  \0
00b0020   ^ 231   I  \0  \0  \0  \0  \0  \b  \0  \0  \0  \0  \0  \0  \0
00b0030 022 001  \0  \0  \0  \0  \0  \0 030 300  \t  \0 300  \0  \0  \0
00b0040  \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  \0
00bbf00 030 300  \t  \0 300  \0  \0  \0  \0  \0  \v  \0 300  \0  \0  \0
00bbf10 300   h   H  \0  \0  \0  \0  \0  \0  \0  \v  \0 300  \0  \0  \0
00bbf20  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
00bbf30   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  \0
00b0040  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0

如上面標註的一樣,這兩個地址都是合法的。簡單說明一下:

最後看一下 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.hex
0017950   a   c   e   B   u   f   u   n   k   n   o   w   n   (   z   h
0017960   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/maps
00400000-00482000 r-xp 00000000 fd:01 672257                             /root/chainmaker/my-mem/my-mem
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

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

  1. /proc//maps 簡要分析:

https://www.cnblogs.com/arnoldlu/p/10272466.html

作者簡介

邵珠光

騰訊後臺開發工程師

騰訊後臺開發工程師,目前主要負責區塊鏈開源底層平臺 - 長安鏈的設計與研發工作,工作中喜歡總結與思考,對技術有獨特的熱愛。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PN-tyUEez2rQT9aitxjy2g