简化垃圾回收器与底层编程探索
在编程领域,垃圾回收是一项重要的技术,它能帮助我们更高效地管理内存。同时,了解计算机底层的指令格式和电子原理,也有助于我们深入理解计算机的工作机制。下面我们将详细介绍一个简化的垃圾回收器,并探讨计算机底层的相关知识。
简化垃圾回收器
这个简化的垃圾回收器有诸多限制,主要用于让大家对垃圾回收的工作原理有一个直观的认识。
限制条件
-
必须在程序开始时通过调用
gc_init显式初始化回收器。 -
需通过调用
gc_scan显式请求回收操作。 - 所有指针必须按 16 字节对齐。
- 所有指针必须存在于栈或堆中才能被正确扫描。
- 所有指针必须指向一段内存的起始位置才能被正确扫描。
- 仅适用于单栈(即单线程代码)。
- 不能与其他分配器一起使用。
- 在扫描过程中,该实现会临时使已分配的内存翻倍。
代码组织
代码主要由以下几个文件组成:
-
gc_defs.s
:包含程序其余部分所需的
.equ
指令。
.globl BRK_SYSCALL
.globl HEADER_SIZE, HDR_IN_USE_OFFSET, HDR_SIZE_OFFSET
.equ BRK_SYSCALL, 12
.equ HEADER_SIZE, 16
.equ HDR_IN_USE_OFFSET, 0
.equ HDR_SIZE_OFFSET, 8
-
gc_globals.s:包含所有垃圾回收代码共享的全局变量。
.include "gc_defs.s"
.globl heap_start, heap_end, stack_start, stack_end
.globl pointer_list_start, pointer_list_end, pointer_list_current
.section .data
heap_start:
.quad 0
heap_end:
.quad 0
stack_start:
.quad 0
stack_end:
.quad 0
.equ pointer_list_start, heap_end # These are the same
pointer_list_end:
.quad 0
pointer_list_current:
.quad 0
-
gc_init.s:包含gc_init函数的代码,用于初始化垃圾回收器。
.globl gc_init
.section .text
gc_init:
# Assume %rbp has the previous stack frame,
# and is properly aligned
movq %rbp, stack_start
# Save the location of the heap
movq $BRK_SYSCALL, %rax
movq $0, %rdi
syscall
movq %rax, heap_start
movq %rax, heap_end
ret
-
gc_allocate.s:包含分配器函数gc_allocate,其工作方式与常见的分配器函数类似,还会将内存清零以避免意外引用指针。
.include "gc_defs.s"
.globl gc_allocate
.section .text
# Register usage:
# - %rdx - size requested
# - %rsi - pointer to current memory being examined
# - %rcx - copy of heap_end
allocate_move_break:
# Old break is saved in %r8 to return to user
movq %rcx, %r8
# Calculate where we want the new break to be
# (old break + size)
movq %rcx, %rdi
addq %rdx, %rdi
# Save this value
movq %rdi, heap_end
# Tell Linux where the new break is
movq $BRK_SYSCALL, %rax
syscall
# Address is in %r8 - mark size and availability
movq $1, HDR_IN_USE_OFFSET(%r8)
movq %rdx, HDR_SIZE_OFFSET(%r8)
# Actual return value is beyond our header
addq $HEADER_SIZE, %r8
movq %r8, %rax
ret
gc_allocate:
enter $0, $0
pushq $0 # Keep stack aligned
pushq %rdi # Save for later
call gc_allocate_internal
# Zero out the block to eliminate false pointers
movq %rax, %rdx # Save original pointer
popq %rcx # Get the size of the block
zeroloop:
movb $0, (%rdx)
incq %rdx
loop zeroloop
leave
ret
gc_allocate_internal:
# Save the amount requested into %rdx
movq %rdi, %rdx
# Actual amount needed is actually larger
addq $HEADER_SIZE, %rdx
# Align %rdx to a 16-byte boundary
addq $16, %rdx # Advance 16 bytes
andq $0xfffffffffffffff0, %rdx # Clear last bits
# Put heap start/end in %rsi/%rcx
movq heap_start, %rsi
movq heap_end, %rcx
allocate_loop:
# If we have reached the end of memory
# we have to allocate new memory by
# moving the break.
cmpq %rsi, %rcx
je allocate_move_break
# is the next block available?
cmpq $0, HDR_IN_USE_OFFSET(%rsi)
jne try_next_block
# is the next block big enough?
cmpq %rdx, HDR_SIZE_OFFSET(%rsi)
jb try_next_block
# This block is great!
# Mark it as unavailable
movq $1, HDR_IN_USE_OFFSET(%rsi)
# Move beyond the header
addq $HEADER_SIZE, %rsi
# Return the value
movq %rsi, %rax
ret
try_next_block:
# This block didn't work, move to the next one
addq HDR_SIZE_OFFSET(%rsi), %rsi
jmp allocate_loop
-
gc_scan.s:包含垃圾回收函数gc_scan,其具体细节进一步划分到其他文件中。
.globl gc_scan
.section .text
# Parameters - none
# Registers - none
gc_scan:
enter $0, $0
# Setup space for pointer list
call gc_scan_init
# Unmark all objects
call gc_unmark_all
# Get initial set of pointers from base objects
# (stack, data)
call gc_scan_base_objects
# Walk pointer list
call gc_walk_pointers
# Give back space from pointer list
call gc_scan_cleanup
leave
ret
-
gc_scan_init.s:包含初始化扫描的代码,记录当前栈位置并分配足够的内存来存储潜在指针列表。 -
gc_unmark_all.s:将所有分配标记为“未使用”,当检测到指向它们的指针时,会重新标记为已使用。 -
gc_scan_memory.s:对内存位置及其大小进行扫描,查找指向堆的潜在指针。 -
gc_scan_base_objects.s:扫描基础对象(栈和数据段),查找潜在的有效指针。 -
gc_walk_pointers.s:遍历指针列表,检查每个指针是否指向有效的堆分配内存且当前未标记,如果是,则标记该内存为正在使用,并继续扫描该内存以查找更多指针。 -
gc_is_valid_ptr.s:包含判断给定指针是否实际指向堆内存分配起始位置的代码。 -
gc_scan_cleanup.s:释放为保存指针列表而分配的内存。 -
gc_test.c:一个简短的测试程序,展示了如何在 C 代码中使用垃圾回收器。
#include <stdio.h>
void *gc_allocate(int);
void gc_scan();
void gc_init();
volatile void **foo;
volatile void **goo;
int main() {
gc_init();
foo = gc_allocate(500);
fprintf(stdout, "Allocation 1: %x\n", foo);
goo = gc_allocate(200);
foo[0] = goo; // Hold reference to goo so it won't go away
fprintf(stdout, "Allocation 2: %x\n", goo);
gc_scan();
goo = gc_allocate(300);
fprintf(stdout, "Allocation 3: %x\n", goo);
gc_scan();
goo = gc_allocate(200);
fprintf(stdout, "Allocation 4: %x\n", goo);
gc_scan();
// This will be put in the same spot as allocation 3
goo = gc_allocate(200);
fprintf(stdout, "Allocation 5: %x\n", goo);
gc_scan();
foo = gc_allocate(500); // No longer holding reference to allocations 1
& 2
fprintf(stdout, "Allocation 6: %x\n", foo);
gc_scan();
// This will be put in the same spot as allocation 1
goo = gc_allocate(10);
fprintf(stdout, "Allocation 7: %x\n", goo);
// This will be put in the same spot as allocation 2
foo = gc_allocate(10);
fprintf(stdout, "Allocation 8: %x\n", foo);
}
要编译并运行这个程序,可以使用以下命令:
gcc gc_*.* -o gc_test
./gc_test
计算机底层知识
除了垃圾回收器,了解计算机底层的指令格式和电子原理也很有必要。
指令格式
在计算机中,一切都以数字形式存储,包括操作计算机的指令。在 x86 - 64 指令集架构中,指令的长度可变,从单字节到 15 字节不等。
指令的基本格式如下:
| 部分 | 说明 | 长度 |
| ---- | ---- | ---- |
| 前缀 | 修改指令的操作,某些情况下可视为指令的一部分 | 每个前缀 1 字节 |
| 操作码 | 告诉处理器要执行的指令 | 1 - 3 字节 |
| ModR/M | 若操作码需要,则指定使用的寄存器和寻址模式 | 1 字节 |
| SIB | 若操作码需要,用于索引寻址模式 | 1 字节 |
| 位移量 | 若操作码需要,是各种寻址模式中使用的固定值 | 1、2、4 或 8 字节 |
| 立即数 | 若操作码需要,是立即模式的值 | 1、2、4 或 8 字节 |
常见前缀有
rep
系列前缀(如
rep
和
repe
编码为 0xf3,
repne
编码为 0xf2)、
LOCK
前缀(0xf0)和
REX
系列前缀。操作码与助记符不完全匹配,但较为接近,例如
mov
系列指令的操作码会根据操作数的大小和类型而变化。
下面是一些指令编码的示例:
-
movb $8, %ah
编码为
0xb4 0x08
。
-
movq $8, %rbx
编码为
0x48 0xc7 0xc3 0x08 0x00 0x00 0x00
。
电子原理
计算机中的一切最终都通过电信号实现,由一系列微芯片组成,每个微芯片包含大量逻辑门,而逻辑门又由晶体管构成。如果对这方面感兴趣,可以参考以下书籍:
- 《Electronics for Beginners》by Jonathan Bartlett:适合初学者,有很多针对专业人士的内容。
- 《The Art of Electronics》by Paul Horowitz:专业人士必备,详细介绍了许多重要电路及其使用注意事项。
- 《Build Your Own Computer: From Scratch》by David Whipple:展示如何使用 FPGA 设计自己的简化 CPU。
- 《Computer Organization and Design》by David Patterson and John Hennessy 或 《Computer Architecture: A Quantitative Approach》by John Hennessy:是理解 CPU 组织层面工作原理的标准书籍。
通过了解这个简化的垃圾回收器和计算机底层知识,我们能更深入地理解编程和计算机的工作机制,为进一步的学习和实践打下坚实的基础。
简化垃圾回收器与底层编程探索
垃圾回收器工作流程分析
为了更清晰地理解垃圾回收器的工作过程,我们可以用 mermaid 流程图来展示
gc_scan
函数的工作流程:
graph TD;
A[开始] --> B[调用 gc_scan_init];
B --> C[调用 gc_unmark_all];
C --> D[调用 gc_scan_base_objects];
D --> E[调用 gc_walk_pointers];
E --> F[调用 gc_scan_cleanup];
F --> G[结束];
从这个流程图可以看出,
gc_scan
函数主要分为几个关键步骤:
1.
初始化扫描
:
gc_scan_init
记录当前栈位置并分配内存存储潜在指针列表。
2.
标记重置
:
gc_unmark_all
将所有分配标记为“未使用”。
3.
查找初始指针
:
gc_scan_base_objects
扫描栈和数据段,查找潜在有效指针。
4.
遍历指针列表
:
gc_walk_pointers
检查指针是否指向有效堆内存并标记使用,继续扫描更多指针。
5.
清理内存
:
gc_scan_cleanup
释放为指针列表分配的内存。
指令格式深入探讨
在 x86 - 64 指令集架构中,指令格式的复杂性源于其历史发展。从 16 位平台发展到 32 位,再到如今的 64 位平台,同时还要保持向后兼容性,这使得指令格式变得独特。
我们可以通过一个表格来更详细地对比不同指令的编码情况:
| 指令 | 前缀 | 操作码 | ModR/M | SIB | 位移量 | 立即数 | 总编码 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| movb $8, %ah | 无 | 0xb4 | 无 | 无 | 无 | 0x08 | 0xb4 0x08 |
| movq $8, %rbx | 0x48 | 0xc7 | 0xc3 | 无 | 无 | 0x08 0x00 0x00 0x00 | 0x48 0xc7 0xc3 0x08 0x00 0x00 0x00 |
从这个表格中可以更直观地看到不同指令在各个部分的编码差异。例如,
movq
指令由于涉及 64 位操作,需要
REX
前缀(0x48)来使 32 位指令在 64 位平台上正常工作。
电子原理实践建议
对于想要深入了解计算机电子原理的读者,可以按照以下步骤进行实践:
1.
入门学习
:阅读《Electronics for Beginners》,了解基本电子电路知识,进行一些简单的电路实验,如搭建简单的 LED 电路。
2.
深入研究
:学习《The Art of Electronics》,掌握更多重要电路的原理和应用,尝试分析一些复杂电路的工作机制。
3.
实践操作
:参考《Build Your Own Computer: From Scratch》,使用 FPGA 尝试设计自己的简化 CPU。可以从简单的功能开始,逐步增加复杂度。
4.
理论提升
:阅读《Computer Organization and Design》或《Computer Architecture: A Quantitative Approach》,从理论层面深入理解 CPU 的工作原理和架构设计。
通过这些实践步骤,读者可以逐步建立起对计算机电子原理的深入理解,从理论到实践,全面提升自己的知识和技能。
总结
本文详细介绍了一个简化的垃圾回收器,包括其限制条件、代码组织和工作流程。同时,探讨了计算机底层的指令格式和电子原理,通过具体的示例和表格,让读者更直观地理解这些复杂的概念。对于想要深入学习编程和计算机底层知识的读者,这些内容提供了一个很好的起点。无论是垃圾回收器的实现,还是指令格式和电子原理的理解,都有助于我们更好地掌握计算机编程的核心知识,为未来的学习和实践打下坚实的基础。希望读者能够通过本文的介绍,激发对计算机底层技术的兴趣,进一步探索这个充满挑战和乐趣的领域。
超级会员免费看
1982

被折叠的 条评论
为什么被折叠?



