首页 > 解决方案 > Common Lisp 中的对象内存布局

问题描述

我知道 Common Lisp 不鼓励程序员接触原始内存,但我想知道是否可以查看对象是如何在字节级别存储的。当然,垃圾收集器在内存空间中移动对象,随后两次调用函数(obj-as-bytes obj)可能会产生不同的结果,但让我们假设我们只需要内存快照。你将如何实现这样的功能?

我对 SBCL 的尝试如下所示:

(defun obj-as-bytes (obj)
  (let* ((addr (sb-kernel:get-lisp-obj-address obj)) ;; get obj address in memory
         (ptr (sb-sys:int-sap addr))                 ;; make pointer to this area
         (size (sb-ext:primitive-object-size obj))   ;; get object size 
         (output))
    (dotimes (idx size)
      (push (sb-sys:sap-ref-64 ptr idx) output))     ;; collect raw bytes into list
    (nreverse output)))                              ;; return bytes in the reversed order

咱们试试吧:

(obj-as-bytes #(1)) =>
(0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 111 40 161 4 16 0 0 0 23 1 16 80 0 0 0)

(obj-as-bytes #(2) =>
(0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 95 66 161 4 16 0 0 0 23 1 16 80 0 0 0)

从这个输出中我得出结论,有很多垃圾,它们占用了未来内存分配的空间。我们之所以看到它,是因为它(sb-ext:primitive-object-size obj)似乎返回了一块足够大以容纳对象的内存。

这段代码演示了它:

(loop for n from 0 below 64 collect
  (sb-ext:primitive-object-size (make-string n :initial-element #\a))) =>
(16 32 32 32 32 48 48 48 48 64 64 64 64 80 80 80 80 96 96 96 96 112 112 112 112                                                                                                                               128 128 128 128 144 144 144 144 160 160 160 160 176 176 176 176 192 192 192                                                                                                                                     192 208 208 208 208 224 224 224 224 240 240 240 240 256 256 256 256 272 272                                                                                                                                     272)

因此,如果更准确,obj-as-bytes将给出正确的结果。sb-ext:primitive-object-size但我找不到任何替代方案。

您对如何修复此功能或如何以不同方式实现它有任何建议吗?

标签: memorycommon-lispsbcl

解决方案


正如我在评论中提到的,内存中对象的布局是非常特定于实现的,探索它的工具也必然依赖于实现。

这个答案讨论了 SBCL 的 64 位版本的布局,并且仅适用于具有“wide fixnums”的 64 位版本。我不确定这两件事是按什么顺序到达 SBCL 的,因为早在 SBCL 和 CMUCL 分歧之前,我就没有认真研究过这些。

这个答案也可能是错误的:我不是 SBCL 开发人员,我只是添加它,因为没有人拥有(我怀疑正确标记问题可能对此有所帮助)。

下面的信息来自查看GitHub mirror,它似乎与规范源非常最新,但速度要快得多。

指针、直接对象、标签

[来自这里的信息。] SBCL 在两个字的边界上分配。在 64 位系统上,这意味着任何地址的低四位始终为零。这些低四位用作标签(文档称其为“低标签”)来告诉您单词的其余部分是什么类型的东西。

  • xyz 0 的低标记意味着单词的其余部分是一个固定编号,特别是xyz将是固定编号的低位,而不是标记位。这意味着有 63 位可用于 fixnums 并且 fixnum 添加是微不足道的:您不需要屏蔽任何位。
  • xy 01的低标记意味着单词的其余部分是其他直接对象。低标签右侧的一些位(我认为 SBCL 将其称为“宽标签”,尽管我对此感到困惑,因为该术语似乎以两种方式使用)将说明直接对象是什么。直接对象的示例是字符和单浮点数(在 64 位平台上!)。
  • 其余的低标签模式是xy 11,它们都意味着事物是指向某些非立即对象的指针:
  • 0011 是某事的一个实例;
  • 0111 是一个缺点;
  • 1011是一个函数;
  • 1111是另一回事。

缺点

因为 conses 不需要任何额外的类型信息(一个 cons 就是一个 cons),所以 lowtag 就足够了:一个 cons 就是内存中的两个单词,每个单词又都有 lowtags &c。

其他非直接对象

我认为(但不确定)所有其他非立即对象都有一个词来说明它们是什么(也可以称为“widetag”)和至少一个其他词(因为分配是在两个词的边界上) . 我怀疑函数的特殊标记意味着函数调用可以跳转到函数代码的入口点。

看着这个

room.lisp有一个很好的函数hexdump,它知道如何打印出非立即对象。基于此,我写了一个小垫片(下),它试图告诉你有用的东西。这里有些例子。

> (hexdump-thing 1)
lowtags:    0010
fixnum:     0000000000000002 = 1

1是一个固定数字,它的表示只是如上所述右移一位。请注意,在这种情况下,低标签实际上包含整个值!

> (hexdump-thing 85757)
lowtags:    1010
fixnum:     0000000000029DFA = 85757

...但在这种情况下不是。

> (hexdump-thing #\c)
lowtags:    1001
immediate:  0000000000006349 = #\c
> (hexdump-thing 1.0s0)
lowtags:    1001
immediate:  3F80000000000019 = 1.0

字符和单个浮点数是直接的:lowtag 左侧的一些位告诉系统它们是什么,我想?

> (hexdump-thing '(1 . 2))
lowtags:    0111
cons:       00000010024D6E07 : 00000010024D6E00
10024D6E00: 0000000000000002 = 1
10024D6E08: 0000000000000004 = 2
> (hexdump-thing '(1 2 3))
lowtags:    0111
cons:       00000010024E4BC7 : 00000010024E4BC0
10024E4BC0: 0000000000000002 = 1
10024E4BC8: 00000010024E4BD7 = (2 3)

缺点。在第一种情况下,您可以在 cons 的两个字段中看到两个固定数字作为立即值。其次,如果您解码了第二个字段的低标签,它将是0111:这是另一个缺点。

> (hexdump-thing "")
lowtags:    1111
other:      00000010024FAE8F : 00000010024FAE80
10024FAE80: 00000000000000E5
10024FAE88: 0000000000000000 = 0
> (hexdump-thing "x")
lowtags:    1111
other:      00000010024FC22F : 00000010024FC220
10024FC220: 00000000000000E5
10024FC228: 0000000000000002 = 1
10024FC230: 0000000000000078 = 60
10024FC238: 0000000000000000 = 0
> (hexdump-thing "xyzt")
lowtags:    1111
other:      00000010024FDDAF : 00000010024FDDA0
10024FDDA0: 00000000000000E5
10024FDDA8: 0000000000000008 = 4
10024FDDB0: 0000007900000078 = 259845521468
10024FDDB8: 000000740000007A = 249108103229

字符串。它们有一些类型信息、一个长度字段,然后将两个字符打包成一个单词。单个字符串需要四个单词,与四个字符的字符串相同。您可以从数据中读取字符代码。

> (hexdump-thing #())
lowtags:    1111
other:      0000001002511C3F : 0000001002511C30
1002511C30: 0000000000000089
1002511C38: 0000000000000000 = 0
> (hexdump-thing #(1))
lowtags:    1111
other:      00000010025152BF : 00000010025152B0
10025152B0: 0000000000000089
10025152B8: 0000000000000002 = 1
10025152C0: 0000000000000002 = 1
10025152C8: 0000000000000000 = 0
> (hexdump-thing #(1 2))
lowtags:    1111
other:      000000100252DC2F : 000000100252DC20
100252DC20: 0000000000000089
100252DC28: 0000000000000004 = 2
100252DC30: 0000000000000002 = 1
100252DC38: 0000000000000004 = 2
> (hexdump-thing #(1 2 3))
lowtags:    1111
other:      0000001002531C8F : 0000001002531C80
1002531C80: 0000000000000089
1002531C88: 0000000000000006 = 3
1002531C90: 0000000000000002 = 1
1002531C98: 0000000000000004 = 2
1002531CA0: 0000000000000006 = 3
1002531CA8: 0000000000000000 = 0

简单向量的处理方式相同:标题、长度,但现在每个条目当然需要一个词。最重要的条目是固定编号,您可以在数据中看到它们。

所以它继续。


执行此操作的代码

可能是错误的,它的早期版本肯定不喜欢小的 bignums(我认为hexdump不喜欢它们)。如果您想要真正的答案,请阅读源代码或询问 SBCL 人员。其他实现是可用的,并且会有所不同。

(defun hexdump-thing (obj)
  ;; Try and hexdump an object, including immediate objects.  All the
  ;; work is done by sb-vm:hexdump in the interesting cases.
  #-(and SBCL 64-bit)
  (error "not a 64-bit SBCL")
  (let* ((address/thing (sb-kernel:get-lisp-obj-address obj))
         (tags (ldb (byte 4 0) address/thing)))
    (format t "~&lowtags: ~12T~4,'0b~%" tags)
    (cond
      ((zerop (ldb (byte 1 0) tags))
       (format t "~&fixnum:~12T~16,'0x = ~S~%" address/thing obj))
      ((= (ldb (byte 2 0) tags) #b01)
       (format t "~&immediate:~12T~16,'0x = ~S~%" address/thing obj))
      ((= (ldb (byte 2 0) tags) #b11)   ;must be true
       (format t "~&~A:~12T~16,'0x : ~16,'0x~%"
               (case (ldb (byte 2 2) tags)
                 (#b00 "instance")
                 (#b01 "cons")
                 (#b10 "function")
                 (#b11 "other"))
               address/thing (dpb #b0000 (byte 4 0) address/thing))
       ;; this tells you at least something (and really annoyingly
       ;; does not pad addresses on the left)
       (sb-vm:hexdump obj))
      ;; can't happen
      (t (error "mutant"))))
  (values))

推荐阅读