eBPF'yi muhtemelen daha önce duymuşsundur; Linux kernel'da sunduğu genişletilmiş observability ve düşük seviyeli kontrol imkânlarıyla her geçen gün daha da popülerleşiyor.
Peki eBPF'in Go runtime'ının içine de bakabildiğini biliyor muydun?
Okumaya devam et, eBPF uprobe'larını kullanarak goroutine yaşam döngüsü olaylarını ve bellek tahsislerini tam gerçekleştiği anda nasıl izleyebileceğimizi göstereceğim. Bu sayede Go scheduler'ının kaputun altında neler yaptığını gerçek zamanlı olarak görebileceğiz.
xgotop. xgotop, goroutine durum değişikliklerini ve bellek tahsislerini neredeyse gerçek zamanlı olarak gözlemleyen bir Go runtime izleme aracı.
Bu iki bölümlük bir yazı dizisi olacak. Bu yazıda xgotop'un tasarım ve geliştirme sürecindeki düşünce akışımı paylaşacağım; ayrıca Go runtime internals'ının bazı yönlerine değinecek ve eBPF programları için PoC yazmak amacıyla bpftrace'i nasıl kullanacağımızı anlatacağım. İkinci yazıda ise bpftrace PoC'sini eBPF ringbuffer'lar, hashmap'ler, LRU cache'ler ve yapılandırılabilir örnekleme kullanan; C, Go ve cilium/ebpf-go ile yazılmış bağımsız bir projeye dönüştüreceğiz.
Bir Goroutine'in Hayatı
Tüm insanlar (ve eğer bir AI agent isen, sen de) gibi goroutine'ler de doğar, etrafında bir şeyler biriktirir ve sonunda bu biriktirdiklerini geldikleri yere geri verip ölürler. Yaşamları boyunca binlerce kez farklı durumlar arasında geçiş yaparlar. Bu değişimler bir goroutine schedule edildiğinde, runtime'da park ettiğinde, bir syscall'a girip çıktığında, preempt edildiğinde veya sonlandığında gerçekleşir. Bellek tahsisi ise goroutine kullanıcı kodu çalıştırırken meydana gelebilen ayrı bir runtime olayıdır.

Bizden farklı olarak goroutine'ler çok daha kısa yaşar; bu yüzden sadece terminale log basarak hayatlarını gözlemlemek oldukça zordur. Örneğin bir milisaniye içindeki olay sıralamasını gözden kaçırmak çok kolaydır. Daha detaylı bakabilmek için log'ların ötesinde bir şeye ihtiyacımız var. Peki bir goroutine durum değiştirdiğinde veya bellek operasyonu yaptığında bize bunu söyleyecek bir arayüz olsaydı?
Şanslıyız ki böyle bir arayüz mevcut. Go runtime'ının bir parçası olarak goroutine'ler, bu operasyonları başlatmak için runtime kütüphanesinde belirli fonksiyonları çağırır. Bu çağrılar userspace'te gerçekleşir. Bir yanda da eBPF var; belirli bir fonksiyonun çalışmaya başlamadan ve bittikten sonra rastgele kod çalıştırmamıza imkân tanıyor.
Belirli Go runtime fonksiyonlarını eBPF userspace probe'ları ile bir araya getirerek goroutine'lerin basit bir şekilde trace'ini yapabiliriz.
Go Runtime'ı İnceleyelim
İşe goroutine yaşam döngüsünü daha iyi anlamakla başlayalım. Goroutine, Go fonksiyonları üzerinde bir soyutlamadır. Go runtime içinde hafif (lightweight) bir thread olarak çalışır ve belirli bir fonksiyon ve bir parent goroutine ile başlatılır. Tüm goroutine'lerin bir parent'ı vardır ve bu parent başka bir goroutine'dir; tek istisna, bir Go binary'sini çalıştırdığında main fonksiyonuna bağlanan ilk goroutine'dir.
Her goroutine'in birden fazla çocuğu olabilir; dolayısıyla parent ve çocuklar arasındaki ilişkinin Linux process tree'sine benzediği söylenebilir.
Goroutine'ler Go runtime'da g adıyla tanımlı bir struct ile temsil edilir:
src/runtime/runtime2.go1type g struct {
2 goid uint64
3 parentGoid uint64 // goid of goroutine that created this goroutine
4 startpc uintptr // pc of goroutine function
5 atomicstatus atomic.Uint32 // atomic status code
6 ...
7}
Görüldüğü gibi g, kendi ve parent'ının ID'sini, mevcut durumunu, başlatıldığı fonksiyonu ve bu yazının kapsamı dışında kalan başka runtime ile ilgili bilgileri tutuyor.
Aşağıda bir goroutine'in olabileceği durumların listesi var. Bu durumlar g.atomicstatus içinde saklanır (kullanılmayan ve scan olanları atlıyorum, tam listeye buradan ulaşabilirsin):
| Durum | Kullanıcı Kodu Çalıştırır mı | Run Queue'da Mi | Notlar |
|---|---|---|---|
_Gidle | Hayır | Hayır | Yeni tahsis edildi, henüz initialize edilmedi |
_Grunnable | Hayır | Evet | Çalışmaya hazır |
_Grunning | Evet | Hayır | Aktif olarak çalışıyor |
_Gsyscall | Hayır | Hayır | Syscall çalıştırıyor, bir M'e atanmış, g.m.p'ye dokunmamalı |
_Gwaiting | Hayır | Hayır | Runtime'da blok olmuş, genellikle tekrar hazır hale getirilebilmesi için bir yere kaydedilmiş |
_Gdead | Hayır | Hayır | Sonlanmış, free list'te veya initialize ediliyor |
_Gcopystack | Hayır | Hayır | Stack taşınıyor |
_Gpreempted | Hayır | Hayır | suspendG preemption için askıya alınmış, henüz hazır hale getiren bir tarafa ait değil |
_Gleaked | Hayır | Hayır | GC tarafından yakalanmış leak olmuş goroutine |
_Gdeadextra | Hayır | Hayır | cgo callback'leri için ekstra bir M'e bağlanmış _Gdead goroutine |
Bu değişkenler bize bir goroutine'in yaşam döngüsünü sürekli izlemek için yeterli imkânı sunuyor. Ancak bunu yapmaya başlamadan önce, goroutine'leri yaratan ve durumlarını değiştiren fonksiyonların hangileri olduğunu bilmemiz gerek. Pratikte goroutine yaşam döngüsü değişikliklerini, runtime'daki durum geçiş noktalarına hook'lar takarak izleyebiliriz.
Go runtime'daki durum değiştiren tüm fonksiyonları tek tek bulup hook'lamak yerine küçük bir hile yapabiliriz: runtime.casgstatus, anahtar düşük seviyeli yardımcılardan biridir çünkü scheduler'daki birçok geçiş bu fonksiyon üzerinden gerçekleşir. casgstatus'a hook takarak, bazı sıcak yollar (hot path'ler) doğrudan atomic CAS1 operasyonları veya özelleşmiş yardımcılar kullansa da, önemli goroutine durum değişikliklerinin büyük bir kısmını gözlemleyebiliriz.
src/runtime/proc.go1func casgstatus(gp *g, oldval, newval uint32)
Bellek operasyonları için goroutine'lerin kullandığı runtime fonksiyonları şunlar:
runtime.makemap_small:hint <= abi.MapGroupSlotskoşulunu sağlayanmake(map[k]v)vemake(map[k]v, hint)ifadeleri için yeni bir runtime map objesi oluşturur.runtime.makemap:make(map[k]v, hint)ifadesi için yeni bir runtime map objesi oluşturur.
runtime.makeslice: Yeni bir runtime slice veri yapısı oluşturur ve alttaki diziye bellek tahsis eder.runtime.newobject: Verilen tipte yeni bir rastgele Go objesi oluşturur.
Belki bellek deallocation'ı için fonksiyon(lar) eklemediğimi fark etmişsindir. Belirli bir objenin (özel tip vb.) deallocate edilip edilmediğini takip etmek önemsiz (trivial) bir operasyon değil. Bunun nasıl gerçekleştiğine ve nasıl gözlemlenebileceğine ilerleyen bir yazıda bakabiliriz.
Yaygın scheduling durumlarını göz önünde bulundurarak _Grunnable, _Grunning, _Gwaiting, _Gsyscall, _Gpreempted ve _Gdead üzerine odaklanan basitleştirilmiş bir goroutine state machine çizebiliriz. _Gidle'ı ana diyagrama dahil etmiyorum çünkü o noktada goroutine henüz sadece tahsis edilmiş ama initialize edilmemiş oluyor; _Gcopystack, _Gleaked ve _Gdeadextra gibi özelleşmiş durumları da atlıyorum.
Artık bu fonksiyonların argümanlarına gerçek zamanlı olarak daha yakından bakabilmek için bazı eBPF hook'ları yazmaya hazırız!
Fikrimizi Doğrulamak için bpftrace Kullanmak
eBPF ile ilgili yeni bir fikrim olduğunda, tam bir eBPF programı tasarımına ve implementasyonuna dalmadan önce aslında bpftrace ile basit bir PoC yazmayı tercih ederim. Bu, fikrin uygulanabilir olup olmadığını çok daha hızlı görmemi sağlıyor.
Aşina olmayanlar için: bpftrace, özel bir script formatını birden fazla hook içeren eksiksiz bir eBPF programına dönüştüren bir program/parser/transpiler'dır. Bazı güzel kolaylıkları var; örneğin C-stili struct'lar tanımlayıp karmaşık kernel içi ve runtime içi veri yapılarını pointer dereference ile çözebilirsin ve bpftrace güvenli bellek erişimini otomatik olarak halleder:
1struct g {
2 uint64_t goid; // offset=152 size=8
3};
4
5uprobe:./testserver:runtime.newobject
6{
7 $goroutine = reg("r28");
8 $g = (struct g *)($goroutine);
9 $goroutine_id = $g->goid;
10 ...
11}
Aynı ((struct g *)reg("r28"))->goid ifadesi eBPF C kodunda örneğin 2 ekstra değişken ve BPF CO-RE bellek kopyalama çağrısı gerektirirdi.
Bir diğer güzel özellik de mevcut register değerlerine doğrudan reg() fonksiyonu üzerinden ulaşabilmemiz. Go fonksiyon çağrı yönteminde (call convention) argümanlar için register'ları kullandığı için2, bu argümanlara reg ve özel tip tanımlarıyla doğrudan erişebiliyoruz. Yukarıdaki örnekte olduğu gibi, arm64 mimarisinde mevcut goroutine objesini almak için reg("r28") kullanılıyor ve onun goid alanına erişmek için özel tanımlanmış bir struct g tipi kullanılıyor.
Biraz Daha Go Runtime Sihri
Hangi Go runtime fonksiyon argümanlarına bakabileceğimize göz atalım. Bir hatırlatma olarak, hook'layacağımız fonksiyonlar şunlar:
src/runtime/...1func casgstatus(gp *g, oldval, newval uint32)
2func newobject(typ *_type) unsafe.Pointer
3func makeslice(et *_type, len, cap int) unsafe.Pointer
4func makemap(t *abi.MapType, hint int, m *maps.Map) *maps.Map
Daha önce runtime.g struct'ına göz atmıştık; buradaki yeni runtime tipleri: abi.MapType ve aslında abi.Type'a bir referans olarak tanımlanmış olan runtime._type:
src/runtime/type.go1type _type = abi.Type
abi.Type
Go ABI'sındaki bir Go tipinin runtime temsili. Tipin toplam boyutunu, türünü (kind), hizalamasını, string biçimini vb. tanımlar.
src/internal/abi/type.go 1type Type struct {
2 Size_ uintptr
3 PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
4 Hash uint32 // hash of type; avoids computation in hash tables
5 TFlag TFlag // extra type information flags
6 Align_ uint8 // alignment of variable with this type
7 FieldAlign_ uint8 // alignment of struct field with this type
8 Kind_ Kind // what kind of type this is (string, int, ...)
9 // function for comparing objects of this type
10 // (ptr to object A, ptr to object B) -> ==?
11 Equal func(unsafe.Pointer, unsafe.Pointer) bool
12 GCData *byte
13 Str NameOff // string form
14 PtrToThis TypeOff // type for pointer to this type, may be zero
15}
Buradaki Kind_ alanı, bu tipin ne tür bir tip olduğunu tanımlar ve Go kaynak kodunda şu şekilde tanımlanmıştır:
src/internal/abi/type.go 1// A Kind represents the specific kind of type that a Type represents.
2// The zero Kind is not a valid kind.
3type Kind uint8
4
5const (
6 Invalid Kind = iota
7 Bool
8 Int
9 Int8
10 Int16
11 Int32
12 Int64
13 Uint
14 Uint8
15 Uint16
16 Uint32
17 Uint64
18 Uintptr
19 Float32
20 Float64
21 Complex64
22 Complex128
23 Array
24 Chan
25 Func
26 Interface
27 Map
28 Pointer
29 Slice
30 String
31 Struct
32 UnsafePointer
33)
abi.MapType
Bir Go map'inin asıl metadata tanımı; key ve value'ların native/özel tiplerini, func(unsafe.Pointer, uintptr) uintptr imzalı hash fonksiyonunu vb. içerir.
src/internal/abi/map.go 1type MapType struct {
2 Type
3 Key *Type
4 Elem *Type
5 Group *Type
6 Hasher func(unsafe.Pointer, uintptr) uintptr
7 GroupSize uintptr
8 KeysOff uintptr
9 KeyStride uintptr
10 ElemsOff uintptr
11 ElemStride uintptr
12 ElemOff uintptr
13 Flags uint32
14}
Buradaki *Type, yukarıda açıklanan abi.Type'a bir pointer'dır.
Hook'layacağımız fonksiyonlara geri dönersek, bpftrace/eBPF'te fonksiyon argümanlarını çözerek goroutine'ler hakkında öğrenebileceklerimiz şunlar:
casgstatus-> Goroutine ID'si, parent goroutine ID'si, eski durum, yeni durumnewobject-> Yeni objenin bellek boyutu ve türü (kind)makeslice-> Yeni slice'ın uzunluğu (length) ve kapasitesi (capacity), toplam bellek boyutu ve türümakemap-> Hint (başlangıç kapasitesi), yeni map'in key/value tipleri (toplam boyutları ve kind'ları) ve map'in toplam bellek boyutu ile türü
xgotop.bt bpftrace Script'i
Şimdi bu Go runtime fonksiyonlarını hook'layacak basit bir bpftrace programı/script'i yazmayı deneyelim. Önce bpftrace kodumuzu test edebileceğimiz bir Go programına ihtiyacımız var. Bunun için son derece basit bir HTTP server kullanacağız:
cmd/testserver/main.go 1package main
2
3import (
4 "fmt"
5 "net/http"
6
7 "github.com/gorilla/mux"
8)
9
10func main() {
11 r := mux.NewRouter()
12 r.HandleFunc("/books/{title}/page/{page}", GetBook)
13 http.ListenAndServe(":80", r)
14}
15
16func GetBook(w http.ResponseWriter, r *http.Request) {
17 vars := mux.Vars(r)
18 title := vars["title"]
19 page := vars["page"]
20 fmt.Fprintf(w, "You've requested the book: %s on page %s\n", title, page)
21}
Bu testserver programını build edip çalıştırdığında ve curl, httpie ya da favori HTTP istemcinle bir HTTP isteği gönderdiğinde çalıştığını göreceksin:
1$ go build -o testserver ./cmd/testserver
2
3$ ./testserver &
4
5$ curl localhost/books/test123/page/45
6You've requested the book: test123 on page 45
Ama tabii ki asıl mesele bu değil. Şimdi asıl kodu, yani bpftrace kodunu yazabiliriz. casgstatus, newobject, makeslice ve makemap fonksiyonlarına hook takıp register'ları kullanarak gerçek fonksiyon argümanlarını çözecek ve belirli zaman noktalarında goroutine yaşam döngüsü olaylarını gözlemleyeceğiz.
Aşağıda goroutine tracer'ımız için bpftrace tabanlı eksiksiz proof-of-concept implementasyonu var:
xgotop.bt 1struct g {
2 uint8_t _pad1[152];
3 uint64_t goid; // offset=152 size=8
4 uint8_t _pad2[112];
5 uint64_t parentGoid; // offset=272 size=8
6 uint8_t _pad3[16];
7 uint64_t startpc; // offset=296 size=8
8};
9
10struct _type {
11 uint64_t size; // offset=0 size=8
12 uint8_t _pad1[15];
13 uint8_t kind; // offset=23 size=1
14}
15
16struct mapType {
17 uint8_t _pad1[48];
18 struct _type *key; // offset=48 size=8
19 struct _type *elem; // offset=56 size=8
20};
21
22// func casgstatus(gp *g, oldval, newval uint32)
23uprobe:./testserver:runtime.casgstatus
24{
25 $gp = (struct g *)reg("r0");
26 $goid = $gp->goid;
27 $parentGoid = $gp->parentGoid;
28 $startpc = $gp->startpc;
29
30 $goroutine = (struct g *)reg("r28");
31 $goroutine_id = $goroutine->goid;
32
33 $status = reg("r2");
34
35 printf("[%llu] pid=%d goid=%llu parentGoid=%llu startpc=%llx status=%llu [goroutine_id=%lld]\n", nsecs, pid, $goid, $parentGoid, $startpc, $status, $goroutine_id);
36}
37
38// func newobject(typ *_type) unsafe.Pointer
39uprobe:./testserver:runtime.newobject
40{
41 $g = (struct g *)reg("r28");
42 $goroutine_id = $g->goid;
43
44 $typ = (struct _type *)(reg("r0"));
45 $size = $typ->size;
46 $kind = $typ->kind;
47
48 printf("[%llu] g:%lld:runtime.newobject(size=%llu, kind=%llu)\n", nsecs, $goroutine_id, $size, $kind);
49}
50
51// func makeslice(et *_type, len, cap int) unsafe.Pointer
52uprobe:./testserver:runtime.makeslice
53{
54 $g = (struct g *)reg("r28");
55 $goroutine_id = $g->goid;
56
57 $typ = (struct _type *)(reg("r0"));
58 $size = $typ->size;
59 $kind = $typ->kind;
60
61 $len = reg("r1");
62 $cap = reg("r2");
63
64 printf("[%llu] g:%lld:runtime.makeslice(size=%llu, kind=%llu, len=%llu, cap=%llu)\n", nsecs, $goroutine_id, $size, $kind, $len, $cap);
65}
66
67// func makemap(t *abi.MapType, hint int, m *maps.Map) *maps.Map
68uprobe:./testserver:runtime.makemap
69{
70 $g = (struct g *)reg("r28");
71 $goroutine_id = $g->goid;
72
73 $maptyp = (struct mapType *)(reg("r0"));
74
75 $keysize = $maptyp->key->size;
76 $keykind = $maptyp->key->kind;
77 $valsize = $maptyp->elem->size;
78 $valkind = $maptyp->elem->kind;
79
80 $hint = reg("r1");
81
82 printf("[%llu] g:%lld:runtime.makemap(keysize=%llu, keykind=%llu, valsize=%llu, valkind=%llu, hint=%llu)\n", nsecs, $goroutine_id, $keysize, $keykind, $valsize, $valkind, $hint);
83}
Her hook'un nasıl yazıldığına bakalım:
- Dahili Go tiplerine (
runtime.g,abi.Typeveabi.MapType) erişebilmek içing,_type,mapTypestruct'larını tanımladım. Bu struct'lara field padding'i olarak rastgeleuint8_tdizileri eklediğime dikkat et. Bunu yapma sebebim, bu dahili objelerin alanlarının çoğuna ihtiyacımız olmaması; örneğinruntime.g'de sadece ID ve parent ID'ye ihtiyacımız var. Kalan alanları istediğimiz uzunlukta tek baytlık (uint8_t) dizilerle kolayca atlayabiliyoruz. - Runtime fonksiyonlarını hook'lamak için
uprobe(userspace probe) kullandım. Bu fonksiyonlar gecikme (latency) anlamında biraz pahalı olabiliyor, ancak hızlı prototipleme için daha iyi. bpftrace'teki her hook 3 parçaya ayrılarak yazılabilir:
<kind>:<program>:<function>
uprobe:./testserver:runtime.makemap
- Mevcut goroutine'in
gstruct'ını almak içinr28register'ını kullandım3 - İlgili register'ları (
r28,r0, ...) ilgili script içi struct'lara cast ederek fonksiyon çağrı argümanlarını daha detaylı inceledim
testserver arka planda çalışırken xgotop.bt'yi bpftrace ile çalıştırıp ona rastgele HTTP istekleri gönderdiğinde bpftrace'ten ilginç log'lar geldiğini göreceksin:
1$ sudo bpftrace ./xgotop.bt
2Attaching 4 probes...
3[10348086967086] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=1 [goroutine_id=0]
4[10348087023933] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=2 [goroutine_id=0]
5[10348087033386] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=3 [goroutine_id=1]
6[10348087051502] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=2 [goroutine_id=1]
7[10348087054417] g:1:runtime.newobject(size=56, kind=25)
8[10348087059165] g:1:runtime.newobject(size=128, kind=25)
9[10348087066328] g:1:runtime.newobject(size=56, kind=25)
10[10348087069160] g:1:runtime.newobject(size=48, kind=25)
11[10348087071076] g:1:runtime.newobject(size=48, kind=25)
12[10348087076573] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=3 [goroutine_id=1]
13[10348087079363] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=2 [goroutine_id=1]
14[10348087081279] g:1:runtime.newobject(size=8, kind=57)
15[10348087083403] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=3 [goroutine_id=1]
16[10348087085818] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=2 [goroutine_id=1]
17[10348087087234] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=3 [goroutine_id=1]
18[10348087097812] g:1:runtime.newobject(size=144, kind=25)
19[10348087099978] g:1:runtime.newobject(size=48, kind=25)
20[10348087103726] g:1:runtime.newobject(size=32, kind=25)
21[10348087106266] g:0:runtime.newobject(size=440, kind=25)
22[10348087108599] pid=19641 goid=0 parentGoid=0 startpc=0 status=6 [goroutine_id=0]
23[10348088014562] pid=19641 goid=21 parentGoid=1 startpc=1e3710 status=1 [goroutine_id=0]
24[10348088057207] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=3 [goroutine_id=1]
25[10348088069535] pid=19641 goid=1 parentGoid=0 startpc=51cf0 status=2 [goroutine_id=1]
26[10348090552043] pid=19641 goid=21 parentGoid=1 startpc=1e3710 status=2 [goroutine_id=0]
27[10348090586900] g:21:runtime.newobject(size=48, kind=25)
28[10348090607307] g:21:runtime.newobject(size=80, kind=25)
29[10348090610139] g:21:runtime.newobject(size=16, kind=25)
30[10348090611555] g:21:runtime.newobject(size=64, kind=25)
31[10348090615678] g:21:runtime.makeslice(size=128, kind=25, len=4, cap=4)
32[10348090618218] g:21:runtime.newobject(size=88, kind=25)
33[10348090620009] g:21:runtime.makeslice(size=1, kind=8, len=4096, cap=4096)
34[10348090627630] g:21:runtime.makeslice(size=128, kind=25, len=4, cap=4)
35[10348090628921] g:21:runtime.makeslice(size=1, kind=8, len=4096, cap=4096)
Görüyorsun ki bir HTTP isteği gönderildikten sonra:
- İsteği işlemek için
21ID'li yeni bir goroutine spawn edilmiş (0x1e3710olanstartpcmuhtemelentestserver'dakiGetBookfonksiyonuna karşılık geliyor) Uint8tipinde ve4096uzunluğunda iki slice tahsis edilmiş (g:21:runtime.makeslice(size=1, kind=8, len=4096, cap=4096)); bunlar muhtemelen request ve response body buffer'ları- Farklı sebeplerle rastgele struct'lar tahsis edilmiş (
g:21:runtime.newobject(size=..., kind=25))
Görebileceğin gibi, belirli bir Go executable'ı içinde çalışan rastgele goroutine'leri artık trace edebiliyoruz!
Burada durabiliriz; xgotop.bt üzerinde hook'ları değiştirerek, yenilerini ekleyerek, başka bir Go programını inceleyerek vb. istediğin gibi oynayabilirsin.
Bir sonraki bölümde, bu PoC'yi eBPF ringbuffer'ları, hashmap'leri ve event sampling kullanan; C, Go ve cilium/ebpf-go ile yazılmış bağımsız bir projeye dönüştüreceğiz. O zamana kadar takipte kal.
Referanslar
- ozansz/xgotop
- Go runtime:
runtime/proc.go - Go runtime:
runtime/map.go(map internals) - bpftrace
- cilium/ebpf-go
-
Go farklı mimariler için farklı register'lar kullanır: https://go.dev/src/cmd/compile/abi-internal#amd64-architecture https://go.dev/src/cmd/compile/abi-internal#arm64-architecture ↩︎
-
arm64Go ABI'sında r28, mevcut goroutine objesinin bellek konumunu tutan register'dır ↩︎
