Toplantılar genellikle Teams, Zoom, AnyDesk
gibi uzak masaüstü yönetim ve haberleşme araçları üzerinden
yapılıyor. Bunlardan en sevdiğim AnyDesk
diyebilirim, çünkü bağlantı sonrasında eğer uzaktan kontrol izini
verilirse en sorunsuz ve hızlı çalışanı. Genellikle klavye ile kendi bilgisayarınızda hangi harfi yazmaya
çalışıyorsanız karşı taraf da aynı harfi alıyor. Fakat AnyDesk
tarafında yakın zamanda ortaya çıkan güvenlik
bulgusu, daha önce kullanan herkesi korkutmuş durumda artık kimse AnyDesk
üzerinden bağlantı vermek istemiyor.
Elimizde kalan diğer seçeneklerden Teams
ya da Zoom
ile toplantı yapıyorsak ve uzak kontrol verildiyse sıkıntılı dakikalar bizi bekliyor.
Genelde toplantı şu şekilde ilerliyor.
… bey/hanım ben basıyorum ama farklı bir karakter çıkarıyor lütfen {,|,[ tuşuna basabilir misiniz?
Böyle olunca tabi 10 dakika sürecek şeyi 30 dakikada ancak bitirebiliyorsunuz. Diyelim ki bir hata nedeniyle
log kayıtlarında bir inceleme yapmak istiyorsunuz ve sistem docker üzerinde çalışıyor. Aşağıdaki gibi
bir one-liner
yardımıyla şuanda aktif ya da kapanmış olan tüm container
kümesi içinde hata kayıtlarına bakacaksınız.
user@server:~# for c in $(docker ps -a -q); do docker logs "$c" 2>&1 | grep "error:"; done
AnyDesk
ile çalışırsanız ne mutlu, direk karşı tarafta genelde sorun çıkmadan yukarıdaki scripti uzak sunucu
üzerinde yazabilirsiniz, hatta kendi bilgisayarınızda yazıp uzak sunucuya copy-paste
yapabilirsiniz. Ama Teams, Zoom
ikilisinden birine denk gelirseniz, genelde copy-paste
çalışmıyor ya da kurum politikası tarafından engellenmiş,
geriye tek seçenek uzak sunucuda bunu kendinizin yazması kalıyor.
Yukarıdaki gibi basit bir scripti hatta bazen daha da basitlerini yazmaya kalkarsanız shell
scriptlerin vazgeçilmezi
olan |,{,},&,[,],-,*,/
gibi karakterlerden mutlaka birisini kullanmak zorunda kalıyorsunuz. Bu karakterlerden birini yazacak tuşa kendi
bilgisayarınızda bastığınızda da karşı tarafta bazen alakasız farklı karakterler bazen de hiç olmayan æß∂ƒğ∑
gibi karakterler çıkabiliyor.
Sonra toplantı yukarıda bahsettiğim dialog şeklinde inanılmaz verimsiz bir şekilde ilerliyor.
Artık bu durum iyice canımı sıkmaya başlayınca elimizdeki araçları ve tabi ki kadim dostumuz vim
‘i kullanarak aşağıdaki gibi
çözüm ürettim.
Bash, Zsh
gibi shell ortamları bize normalde komutları yazdığınız satırın dışında bunları geçerli editör ile yazmanıza da olanak sağlıyor.
ctrl-x, ctrl-e kısayolu
ya da fc komutu ile yazmak istediğiniz kod için size bir editör açılıyor ve orada istediğiniz şekilde
yazıp kaydedip kapattıktan sonra yazdığınız şey otomatik olarak çalıştırılıyor.
Peki shell satırında değil de editör üzerinde yazmak ne değiştirecek sorusu akla gelebilir, eğer editor vim
ise çok şey değiştirebilir.
Tabi öncelikle yukarıda bahsettiğim kısayolların editör olarak vim kullanmasını sağlamamız lazım bunun için kullandığımız shell ortamının
baktığı ortam değişkenini ayarlamak gerekiyor.
user@server:~# EDITOR=vim
Ardından ister ctrl-x, ctrl-e
isterseniz fc
ile komut yazmak için Vim
ortamına geçebilirsiniz. Sonrasında Vim insert mode
içinde bize ascii
tablosundaki sayı karşılığını kullanarak karakter girmemizi sağlıyor.
Detaylar için vim içinde :h i_CTRL-V_digit
yazdığınızda aşağıdaki açıklama geliyor.
With CTRL-V the decimal, octal or hexadecimal value of a character can be
entered directly. This way you can enter any character, except a line break
(<NL>, value 10). There are five ways to enter the character value:
first char mode max nr of chars max value ~
(none) decimal 3 255
o or O octal 3 377 (255)
x or X hexadecimal 2 ff (255)
u hexadecimal 4 ffff (65535)
U hexadecimal 8 7fffffff (2147483647)
Yani ASCII tablosuna bakarak önce insert mode
sonra ctrl-v decimal
değer karşılığını girerek istediğimiz karakterleri yazdırabiliyoruz.
Decimal | Char | Decimal | Char |
---|---|---|---|
33 | ! | 59 | ; |
34 | ” | 60 | < |
35 | # | 61 | = |
36 | $ | 62 | > |
37 | % | 63 | ? |
38 | & | 91 | [ |
39 | ’ | 92 | \ |
40 | ( | 93 | ] |
41 | ) | 94 | ^ |
42 | * | 95 | _ |
43 | + | 123 | { |
44 | , | 124 | | |
45 | - | 125 | } |
46 | . | 126 | ~ |
47 | / | 64 | @ |
58 | : | 96 | ` |
Aşağıda nasıl olduğunu örnek üzerinde çalışırken görebilirsiniz. İlk olarak ctrl-x, ctrl-e
sonra da fc
ile benzer işlemi yapıyorum. Her iki örnekte de |
karakterini yukarıda bahsettiğim vim yöntemi ile yani
insert mode içerisinde ctrl-v 124
ile yapıyorum.
HTTP
servisimiz vardı, buna Server
adını verelim. Yaptığımız işlerden birisi de, görev gereği bu Server
arkadaşı birkaç dakika içinde
yüzlerce defa çağırıp, sonuçları toparlayıp hazırlamaktı. Bunu yapan arkadaşa da bundan sonra Client
diyelim.
Dikkat edilmesi gereken konulardan biri, hem Client hem de Server, Backend
servis olarak çalışan bileşenler,
yani bu HTTP istekleri klasik bir tarayıcı üzerinden gönderilmiyordu. Günlerden bir gün, Client
arkadaşın
sonuçları yeterince hızlı derleyip toparlayamadığını gözlemledik, bu sebeple konuyu derinlemesine inceleyip neler
yapabiliriz çıkardık ve belki çoğu kişinin bildiği bir konuyu farlı bir açıdan ölçmek istedim.
Yazı boyunca kullanılan kod örneklerini buradan edilebilirsiniz.
HTTP bir uygulama protokolü ama gönderilen request ve response aslında TCP
(en azından HTTP/3 haricinde diyelim) protokolü üzerinden taşınıyor.
Tarayıcınız üzerinden bir web sitesine ya da servise istek attığınızda, öncelikle karşı tarafa bir TCP
bağlantısı açılıp
ardından HTTP
protokolü ile ilgili olan çoğu şey TCP
içinde Data
kısmında gönderiliyor.
Durum böyle olunca, HTTP konuşmak için TCP kurallarına uymak gerekiyor, ilk kurallardan biri ise TCP three-way handshake. Yani sunucu ile HTTP konuşmaya başlamadan önce TCP üzerinden el sıkışıp şartlarda anlaşmamız gerekiyor, sonra diğer işlemler takip ediyor.
sequenceDiagram
participant Client
participant Server
Client->>Server: 1. SYN (Synchronize)
Server-->>Client: 1. SYN-ACK (Synchronize-Acknowledge)
Client->>Server: 1. ACK (Acknowledge)
Client->>Server: 1. HTTP Request
Server-->>Client: 1. HTTP Response
Client->>Server: 1. FIN (Finish)
Server-->>Client: 1. ACK (Acknowledge)
Server-->>Client: 1. FIN (Finish)
Client->>Server: 2. SYN (Synchronize)
Server-->>Client: 2. SYN-ACK (Synchronize-Acknowledge)
Client->>Server: 2. ACK (Acknowledge)
Client->>Server: 2. HTTP Request
Server-->>Client: 2. HTTP Response
Client->>Server: 2. FIN (Finish)
Server-->>Client: 2. ACK (Acknowledge)
Server-->>Client: 2. FIN (Finish)
Client-->>Server: 2. ACK (Acknowledge)
Tabi istek bittiğinde de, TCP için yine bağlantıyı kapatmak için yukarıdaki gibi termination
paketlerini de göndermek gerekiyor.
Bu paketlerin her biri tabi hem sunucu hem de istemci tarafında az da olsa ek bir işlemci gücü ve zaman gerektiriyor.
Az sayıda istek alan bir sunucu ya da istemci için bu paketlerin maliyeti pek göze batmazken, bizim gibi yoğun istek altında
kısa sürede işini bitirmesi gereken bir sunucu ya da istemci için oldukça önem arz edebiliyor.
HTTP protokolleri ve kullandığı farklı bağlantı yöntemleri Mozilla
tarafından güzel şekilde özetlenmiş.
HTTP oldukça eski bir protokol, zamanla web kullanımı arttıkça, yukarıdaki durum için aslında çözüm geliştirilmiş ve her seferinde
TCP bağlantısı açık kapatmaktansa bu bağlantıyı koru ve aynı bağlantı üzerinden diğer isteklerini gönder demişler.
Bu yöntemin adına ise Mozilla linkinde ki gibi Persistent Connection
ya da namı diğer keep-alive
deniliyor.
Keep-alive yöntemi ile yukarıdaki gibi grafiği olan 2 adet HTTP Request/Response aşağıdaki gibi gözüküyor.
sequenceDiagram
participant Client
participant Server
Client->>Server: 1. SYN (Synchronize)
Server-->>Client: 1. SYN-ACK (Synchronize-Acknowledge)
Client->>Server: ACK (Acknowledge)
Client->>Server: 1. HTTP Request
Server-->>Client: 1. HTTP Response
Client->>Server: 2. HTTP Request
Server-->>Client: 2. HTTP Response
Client->>Server: 1. FIN (Finish)
Server-->>Client: 1. ACK (Acknowledge)
Server-->>Client: 1. FIN (Finish)
Client-->>Server: 1. ACK (Acknowledge)
Basit bir Python kodu ile HTTP server oluşturalım. Protokol olarak 1.0 yerine 1.1 kullandım çünkü 1.0
bildiğim kadarıyla keep-alive
özelliğini desteklemiyor.
import http.server
import socketserver
import sys
class HttpServer(http.server.SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def handle(self):
self.close_connection = False
while not self.close_connection:
self.handle_one_request()
def run(port):
with socketserver.TCPServer(("", port), HttpServer) as httpd:
print("HTTP 1.1 Persistent Connections Server running at port", port)
httpd.serve_forever()
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
run(port)
Yukarıdaki sunucu gelen isteğe bulunduğu dizindeki dosyaların adlarını listeleyerek cevap dönüyor, bu şekilde olması iyi çünkü mümkün olduğunca basit, başka bir konuda zaman alan bir işlem yapmıyor ve bize asıl ölçmek istediğimiz el sıkışmanın maliyetini daha iyi ortaya çıkarmamazı sağlıyor.
Benze şekilde bu sunucuya, çok sayıda istek gönderecek basit bir curl
komutu hazırlayalım.
#!/bin/sh
PROTOCOL="http"
SERVER="localhost"
PORT=5000
URL="$PROTOCOL://$SERVER:$PORT"
/usr/bin/time curl --silent -H "Connection: close" "$URL" \
"$URL" \
"$URL" \
...
Curl komutunun içinde ise 500 defa aynı URL
tekrarlandı, iki sebebi var. Birincisi her HTTP isteği için
yeniden bir curl process oluşturmak ayrı süre alacağı için bundan kaçınmak istedim, ikincisi ise keep-alive
özelliğini kullanmak
için istekleri aynı curl process içinde göndermek gerekiyor yoksa, keep-alive
curl içinde varsayılan olarak gelse de işlem sonlandığında
eski TCP bağlantısını tutma şansı kalmıyor.
Curl tarafında keep-alive
maalesef varsayılan olarak geldiği için, kapat gibi bir flag yok, bu sebeple aynı şeyi yani her istekte bağlantıyı server
tarafında kapatmaya zorlayan Connection: close
header kullandık.
# python3 http11_server.py 5000
HTTP 1.1 Persistent Connections Server running at port 5000
Local ortamda sunucuyu çalıştırdık, şimdi de test için yazdığımız curl kodunu çalıştırıp sonuçlara bakalım. Aynı zamanda Wireshark
üzerinden de
paket trafiğini kaydedeceğim.
# ./curl-connection-close.sh
1.27 real 0.12 user 0.15 sys
Ortalama olarak yukarıdaki gibi 1.20 saniye civarında işlem bitiyor. Paket trafiğinde ise kaç defa el-sıkışma paketi gönderdiğini görmek için aşağıdaki gibi bir filtre kullandım.
tcp.flags.syn == 1 && tcp.flags.ack == 1
Ekran görüntüsünden de görüldüğü gibi her istek için TCP el-sıkışma işlemleri yapılıyor.
Bu sefer de aynı sunucu tarafını keep-alive kullanan curl komutu ile çağıralım. Curl kodunu aşağıdaki gibi değiştirdim
#!/bin/sh
PROTOCOL="http"
SERVER="localhost"
PORT=5000
URL="$PROTOCOL://$SERVER:$PORT"
/usr/bin/time curl --silent "$URL" \
"$URL" \
"$URL" \
...
Herhangi bir ek header bilgisi eklemeye gerek yok, çünkü dediğim gibi varsayılan değer olarak zaten bu gönderiliyor.
Aslında curl de burada kullandığımızı tarayıcılara benziyor, onlar da varsayılan değer olarak keep-alive
header bilgisini gördüğüm
kadarıyla otomatik olarak gönderiyorlar. Şimdi sonuçları test edelim ve test ederken de yine benzer şekilde paket trafiğini kaydedelim.
# ./curl-keep-alive.sh
0.56 real 0.08 user 0.06 sys
Ortalama olarak 0.60
saniye olarak çıktığını görüyoruz. Paket trafiği ise aşağıdaki gibi gözüküyor.
Beklenildiği gibi, paket trafiğinde 500 tane el-sıkışma paketi yerine sadece 1 adet TCP hand-shake var. Performans olarak da ilk versiyona göre neredeyse, %100 daha hızlı diyebiliriz. Tabi burada yaptığımız mikro ölçümleme oranında canlı ortamlarda iyileşme olmayabilir ama mutlaka bizim gibi sık sık kısa süreli istek gönderen ortamlarda önemli oranda iyileşme sağlanabilir.
Çıkış noktamızdan bahsettiğimde asıl sorunu, NodeJs client olarak diğer sunucuyu çağırdığında yaşıyorduk. Bu yüzden aynı sunucuyu bu sefer de Node kullanarak test edelim.
const http = require('http');
const options = {
hostname: 'localhost',
port: 5000,
path: '/',
method: 'GET'
};
const count = 500;
let requestsCompleted = 0;
function sendRequest() {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
res.on('data', () => {});
res.on('end', () => {
requestsCompleted++;
console.log(`Request ${requestsCompleted} completed.`);
resolve();
});
});
req.on('error', (error) => {
console.error('Request error:', error);
reject(error);
});
req.end();
});
}
async function sendSequentialRequests() {
for (let i = 0; i < count; i++) {
try {
await sendRequest();
} catch (error) {
console.error('Failed to send request:', error);
}
}
console.log('All requests completed.');
}
sendSequentialRequests();
Yukarıdaki kod parçacığında curl benzeri 500 adet sıralı istek atıyoruz ve sonucun ne kadar süre aldığını ölçüyoruz.
# /usr/bin/time node node-client-no-keepalive.js
Request 1 completed.
Request 2 completed.
...
...
All requests completed.
2.20 real 0.54 user 0.25 sys
Paket trafiği yukarıdaki keep-alive olmayan ile aynı şekilde olduğu için tekrar koymuyorum ama özetle, 500 adet TCP handshake mesajı görülüyor ve ortalama
2.20 saniye sürüyor. Açıkçası bu şekilde yapılan bir isteği aynı curl benzeri varsayılan değer olarak keep-alive
yöntemi kullanmasını beklerdim ama öyle değil, görüldüğü
gibi bu kullanılmayıp her defasında yeni bir TCP bağlantı oluşuyor. Peki Node tarafında bunu önlemek için ne yapmak lazım?
Kodu çok az değiştirip keep-alive kullanımı için HTTP Agent kullanıyoruz ve testimizi tekrar çalıştırıyoruz.
const http = require('http');
const options = {
hostname: 'localhost',
port: 5000,
path: '/',
method: 'GET',
agent: new http.Agent({ keepAlive: true })
};
const count = 500;
let requestsCompleted = 0;
function sendRequest() {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
res.on('data', () => {});
res.on('end', () => {
requestsCompleted++;
console.log(`Request ${requestsCompleted} completed.`);
resolve();
});
});
req.on('error', (error) => {
console.error('Request error:', error);
reject(error);
});
req.end();
});
}
async function sendSequentialRequests() {
for (let i = 0; i < count; i++) {
try {
await sendRequest();
} catch (error) {
console.error('Failed to send request:', error);
}
}
console.log('All requests completed.');
}
sendSequentialRequests();
# /usr/bin/time node node-client-keepalive.js
Request 1 completed.
Request 2 completed.
...
...
All requests completed.
0.90 real 0.35 user 0.08 sys
Testin sonucunda ortalama 0.90 saniye çıkıyor, curl testinde çıkan değerden de daha iyi bir oranda iyileşme var diyebiliriz.
Belki burada node tarafında Connection: close
yapısından farklı olarak agent kullanımı ile istemci tarafında bağlantı sonlandırmasının etkisi de olabilir. Ama yine de
oldukça iyi bir oranda iyileşme sağladığı görülüyor.
Node tarafında açıkçası beni yanıltan varsayılan HTTP istek modülünün keep-alive
kullanmamasıydı. Bunu yapmak için agent kullanmanız gerekli. Fakat, tarayıcınız, curl vb. araçlarda ise
bu davranış varsayılan değer olarak ekleniyor.
xychart-beta horizontal
title "Performance Comparison - 500 Requests"
x-axis ["Curl(Keep-alive)", "Curl (Connection-close)","NodeJs(Keep-alive)","NodeJs"]
y-axis "Seconds" 0 --> 5
bar [0.56,1.27, 0.90, 2.20]
Bu iyileştirme sayesinde, biz de neredeyse benzer oranlarda bahsettiğim problemi iyileştirdik diyebilirim. Tabi bunu yaparken ezbere yapmak yerine her zaman arka planda yatan sebepleri irdelemek ve işin temelini anlayarak yapmak daha güzel oluyor.
]]>Özellikle teknik konularda, problem çözmeye çalışırken ya da yeni bir konu, kavram öğrenirken, kendimi bazen bu deyimin
ifade etmeye çalıştığı durum içinde buluyorum. Biraz da copy-paste
yaklaşımı ile yüzeysel olarak geçiştirmeyi sevmeyen
bir yapıda olduğumdan dolayı olabilir.
Bir konu öğrenirken, içinde geçen bilmediğim bir kavram varsa, “bunu bilmiyorum, bir bakayım neymiş”, diye baktıktan hemen sonra,
bir bakmışım tavşan deliği beni bambaşka bir tarafa çıkarmış. Bu yeni konuyu incelerken de, bir bakmışım önümde bambaşka bir konu daha var
ve kendimi ilk düşündüğümden, beklediğimden çok daha farklı konuları öğrenirken ya da incelerken bulmuşum. Kafamda hep
bunun bir lanet
mi yoksa hediye
mi olduğu sorusu olsa da uzun vadede kontrol edebildiğiniz sürece, en azından kendi açımdan
daha faydalı olduğunu gördüm diyebilirim.
Bu yazı serisi 3 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara bu adresten ulaşabilirsiniz.
Tavşan deliğine inişimiz şöyle başladı, elimde NodeJS core dump dosyası var, bellek durumunu incelemek için, önce GDB
ile problemin olduğu ortamda dosyayı yükleyip aşağıdaki gibi açtım. Kafamda en azından Call Stack
beklentisi var.
Onu görüp problemin oluştuğu yer ile ilgili daha net bir bilgi edinebilirim. Bunu GDB üzerinde yapabilmek için backtrace
yani
bt
komutunu çalıştırdım ama aşağıda görüldüğü gibi libc içinde bulunan setjmp
ve raise
dışında NodeJS ile ilgili
bir şey gözükmüyor, sadece ?? ()
maddeleri var.
/tmp # gdb /usr/local/bin/node -c core.f8f32091796c.node.1700129772.28 -q
Reading symbols from /usr/local/bin/node...
[New LWP 28]
[New LWP 30]
[New LWP 29]
[New LWP 33]
[New LWP 31]
[New LWP 32]
[New LWP 34]
Core was generated by `/usr/local/bin/node template.js'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007fa1a8f5a3f2 in setjmp () from /lib/ld-musl-x86_64.so.1
[Current thread is 1 (LWP 28)]
(gdb) bt
#0 0x00007fa1a8f5a3f2 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007fa1a8f5a54d in raise () from /lib/ld-musl-x86_64.so.1
#2 0x00007fa1a8f5b9a9 in ?? () from /lib/ld-musl-x86_64.so.1
#3 0x00007fa1a8faae98 in ?? () from /lib/ld-musl-x86_64.so.1
#4 0x0000000000000000 in ?? ()
(gdb)
Sonuçta elimizdeki tek debugger GDB değil aynı dosyayı, bir de LLDB kurup onun üzerinden deneyip fark olacak mı diye görmek istedim.
Aşağıdaki görüldüğü gibi LLDB herhangi bir sorun olmadan bana stack
durumunu gösterebildi ve oradan da sorunun nerede olduğu anlaşabiliyor.
/tmp # lldb /usr/local/bin/node -c core.f8f32091796c.node.1700129772.28
(lldb) target create "/usr/local/bin/node" --core "core.f8f32091796c.node.1700129772.28"
Core file '/tmp/core.f8f32091796c.node.1700129772.28' (x86_64) was loaded.
(lldb) bt
* thread #1, name = 'node', stop reason = signal SIGABRT
* frame #0: 0x00007fa1a8f5a3f2 ld-musl-x86_64.so.1`__setjmp + 109
frame #1: 0x00007fa1a8f5a54d ld-musl-x86_64.so.1`raise + 55
frame #2: 0x00007fa1a8f30f25 ld-musl-x86_64.so.1`abort + 14
frame #3: 0x00005641a6ef5e55 node`node::Abort() + 37
frame #4: 0x00005641a6e00d27 node`node::OnFatalError(char const*, char const*) + 283
frame #5: 0x00005641a70ed0e2 node`v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) + 82
frame #6: 0x00005641a70ed46f node`v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) + 847
...
...
Peki neden bu stack LLDB'de düzgün gözüküyor da, GDB'de düzgün gözükmüyor?
diye sorduğum an tam olarak tavşan deliğine girmiş olduğum andı.
Terminolojiyi öğrenince debugger araçları için bu Call Stack
çıkarma işinin Stack Unwinding olarak adlandırıldığını öğrendim. Çoğu geliştirdiğimiz program,
belleğe yüklendikten sonra, fonksiyon çağrılarını, lokal değişkenleri stack dediğimiz belleğin bir bölümü üzerinde gerçekleştiriyor.
Kullandığımız IDE, Debugger vb.. araçlar ise stack durumunu analiz edip, bize şuanda bulunan duruma hangi fonksiyon çağrılarının yapılarak
geldiğini, bu fonksiyon çağrıları içinde lokal değişkenleri ve değerlerini, CPU register değerlerini gösterebiliyor. Farkında olmasak da
aslında debug yapıldığı her durumda genelde kullandığımız bir kavram.
Benim asıl merak ettiğim neden, bu işlemi, yani bu hataya hangi fonksiyon çağrılarını yaparak gelmiş bilgisini LLDB gösterebiliyorken
GDB gösteremiyor. Bunu anlamak için kolları sıvayıp, önce bunun GDB ile ilgili bir bug olduğunu düşünüp en son versiyona geçirdim ama değişen bir şey olmadı.
Ardından sorunun NodeJS ile ya da onu derlerken kullanılan compiler parametreleri ile olduğunu düşündüm. LLDB LLVM
ailesinden olduğu için belki NodeJS clang
ile derlendiyse üretilen dosyada aynı aileden olan LLDB üzerinden kolay debug edilebilmesi için ek semboller koyabilir diye düşündüm.
Tezimi, yani kullanılan derleyicinin stack analizine etkisini doğrulamak için aşağıdaki gibi çok basit bir C
kodu yazıp, GCC
ile derledim.
#include <stdio.h>
#include <stdlib.h>
int number=0;
int mul(int num, int times) {
printf("Multiplying numbers: %d,%d\n", num, times);
if (number > 10) {
abort();
}
return num * times;
}
int sub(int num, int sub) {
printf("Subtracting numbers: %d,%d\n", num, sub);
int a = num - sub;
int b = 2;
return mul(a,b);
}
int add(int a, int b) {
printf("Adding numbers: %d,%d\n", a, b);
int c = a + b;
int d = 5;
return sub(c, d);
}
int main(int argc, char* argv[]) {
number=atoi(argv[1]);
printf("Number: %d\n", number);
int result = add(3, 7);
printf("Result: %d\n", result);
return 0;
}
Yukarıdaki kodun amacı, birbirini çağıran birkaç fonksiyon kullanıp, ardından programı çağırırken verilen parametre değerine göre
programın en son adımda hata alıp core-dump almasını sağlamak. Dump oluştuktan sonra GDB ile inceleme yapıp, Call Stack
düzgün gözüküyor mu
bunu inceleyeceğiz. Linux üzerinde dump
almasını sağlayan belirli sinyalleri göndererek programın core-dump üretmesini sağlayabiliriz.
Fakat programı çalıştırıp ardından kill
ile tekrar sinyal göndermekten ise, program içinden bunu abort
ile göndermeyi daha kolay bulduğum
için mul
fonksiyonu içinde rastgele bir değer seçip verilen parametre 10 değerinden büyük ise abort
sinyali gönderip dump almasını sağladım.
Compiler olarak GCC kullanıyorum ve aşağıdaki gibi derledim. Production build işlemlerinde genelde optimizasyonlar açılır,
ben de aynı şekilde optimizasyonu kullansın istedim ve ardından hata verdirecek parametre ile çağırdım,
sonrasında da core dump
dosyası oluştu.
/tmp # gcc -O2 main.c -o main
/tmp # ./main 15
Number: 15
Adding numbers: 3,7
Subtracting numbers: 10,5
Multiplying numbers: 5,2
Aborted (core dumped)
Programın kendini ve üretilen dump dosyasını parametre olarak GDB’ye geçerek, bir debug oturumu başlattım.
Ardından backtrace
komutunu çalıştırarak call stack
görmek istedim, daha önce incelediğimiz NodeJS örneği ile benzer şekilde anlamlı fonksiyon isimleri yerine
??
işareti görüyoruz. Kısacası GDB stack unwinding işlemini yapamadı.
/tmp # gdb -q main -c core-main.1427.e28334d67f07.1705221934
Reading symbols from main...
[New LWP 1427]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f9e872e9d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) bt
#0 0x00007f9e872e9d07 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007f9e872e9e5c in raise () from /lib/ld-musl-x86_64.so.1
#2 0x0000003000000008 in ?? ()
#3 0x00007ffd0a120f00 in ?? ()
#4 0x00007ffd0a120e40 in ?? ()
#5 0x0000000000000000 in ?? ()
Yine aynı dosyayı ve dump dosyasını LLDB ile açtığımda ise aşağıdaki gibi düzgün olarak en azından anlamlı bir fonksiyon ismi add.cold
görebiliyorum.
Diğer fonksiyonlar optimizasyon sonucu compiler tarafından birleştirildi, o yüzden göremiyoruz ama buna rağmen bizim yazdığımız kodun bir parçası call stack içinde gözüküyor.
/tmp # lldb main -c core-main.1427.e28334d67f07.1705221934
(lldb) target create "main" --core "core-main.1427.e28334d67f07.1705221934"
Core file '/tmp/core-main.1427.e28334d67f07.1705221934' (x86_64) was loaded.
(lldb) bt
* thread #1, name = 'main', stop reason = signal SIGABRT
* frame #0: 0x00007f9e872e9d07 ld-musl-x86_64.so.1`__setjmp + 118
frame #1: 0x00007f9e872e9e5c ld-musl-x86_64.so.1`raise + 64
frame #2: 0x00007f9e872bcfa8 ld-musl-x86_64.so.1`abort + 14
frame #3: 0x000055752e9e908f main`add.cold + 5
frame #4: 0x000055752e9e90c2 main`main + 50
frame #5: 0x00007f9e872bcaad ld-musl-x86_64.so.1
(lldb)
Yani sorun compiler yani GCC ile alakalı değil, LLDB GCC ile derlenen bir programın dump dosyasını inceleyip optimizasyonlar açık olsa bile çıkarabildi fakat GDB aynı işlemi yapamadı.
Belki GDB’nin optimizasyonlar açık şekilde derlenmiş programların stack çözümlemesinde yaşadığı bir problem olabilir diye bu sefer optimizasyonları kapatıp o şekilde derleyip tekrar inceledim.
/tmp # gcc -Wall -O0 main.c -o main
/tmp # ./main 15
Number: 15
Adding numbers: 3,7
Subtracting numbers: 10,5
Multiplying numbers: 5,2
Aborted (core dumped)
/tmp # gdb -q main -c core-main.1464.e28334d67f07.1705226726
Reading symbols from main...
[New LWP 1464]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f4e291f0d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) bt
#0 0x00007f4e291f0d07 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007f4e291f0e5c in raise () from /lib/ld-musl-x86_64.so.1
#2 0x0000003000000008 in ?? ()
#3 0x00007ffcae6067e0 in ?? ()
#4 0x00007ffcae606720 in ?? ()
#5 0x00007ffcae606850 in ?? ()
#6 0x0000000000000005 in ?? ()
#7 0x0000000000000002 in ?? ()
#8 0x0000000000000000 in ?? ()
Aynı dump dosyasını LLDB ile incelediğimizde aşağıdaki gibi yine düzgün stack çözümlemesi yapabildiğini görüyoruz.
/tmp # lldb main -c core-main.1464.e28334d67f07.1705226726
(lldb) target create "main" --core "core-main.1464.e28334d67f07.1705226726"
Core file '/tmp/core-main.1464.e28334d67f07.1705226726' (x86_64) was loaded.
(lldb) bt
* thread #1, name = 'main', stop reason = signal SIGABRT
* frame #0: 0x00007f4e291f0d07 ld-musl-x86_64.so.1`__setjmp + 118
frame #1: 0x00007f4e291f0e5c ld-musl-x86_64.so.1`raise + 64
frame #2: 0x00007f4e291c3fa8 ld-musl-x86_64.so.1`abort + 14
frame #3: 0x0000561dda0701ff main`mul + 58
frame #4: 0x0000561dda070251 main`sub + 73
frame #5: 0x0000561dda07029e main`add + 75
frame #6: 0x0000561dda0702f3 main`main + 83
frame #7: 0x00007f4e291c3aad ld-musl-x86_64.so.1
(lldb)
Optimizasyonları kapatsak da işe yaramadı, GDB yine çözümleme işlemini yapamadı.
Bildiğiniz gibi çoğu derlenen programlama dilinde, debug
ve release
olarak iki farklı hedef bulunur. Özellikle geliştirme zamanında
test ortamlarında hataların daha kolay incelenebilmesi için debug
build kullanılır. Bunların içinde bulunan semboller ile aslında debugger
bir nevi çalıştırılan makine/assembly komutlarının kod olarak hangi satıra denk geldiğini kolaylıkla bilir ve siz adım adım ilerlerken,
sizin hangi satırda olduğunuzu değişken değerleri gibi şeyleri gösterebilir.
Belki GDB debug sembolleri olmadığı için, LLDB ise bu konuda özelliği olduğu için doğru analiz yapıyor olabilir, deneyip görelim.
/tmp # gcc -g -Wall -O0 main.c -o main
/tmp # ./main 15
Number: 15
Adding numbers: 3,7
Subtracting numbers: 10,5
Multiplying numbers: 5,2
Aborted (core dumped)
/tmp # gdb -q main -c core-main.1496.e28334d67f07.1705227527
Reading symbols from main...
[New LWP 1496]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f1f223f8d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) bt
#0 0x00007f1f223f8d07 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007f1f223f8e5c in raise () from /lib/ld-musl-x86_64.so.1
#2 0x0000003000000008 in ?? ()
#3 0x00007ffe75a26400 in ?? ()
#4 0x00007ffe75a26340 in ?? ()
#5 0x00007ffe75a26470 in ?? ()
#6 0x0000000000000005 in ?? ()
#7 0x0000000000000002 in ?? ()
#8 0x0000000000000000 in ?? ()
Görüldüğü gibi -g
parametresi ile debug sembollerini de eklememize rağmen yine değişen bir şey olmadı. LLDB tarafında değişen bir şey yok, yine sorunsuzca
stack çözümlemesini yapabiliyor o yüzden tekrar koymaya gerek duymadım.
Bu aşamada araştırma yaparken, problemin bizim kod ile alakalı değil de, stack çıktısının son adımında bulunan çağrılarda görüldüğü gibi
kullanılan libc
kütüphanesi yani musl-libc
ile alakalı olabileceği aklıma geldi. Sonuçta program orada patlıyor ve debugger araçları çözümlemeyi oradan başlayarak
yapmaya çalışıyor. Musl özellikle oldukça hafif ve optimize edilmesiyle bilinen bir kütüphane, içinde debug sembolleri olmasını beklemiyorum, ama bu tarz production ortamları için
dağıttıkları debug sembolleri var mı diye kontrol ettim ve musl-dbg
olarak ayrıca alpine
deposunda bulunduğunu gördüm. Bunu paketi ekledikten sonra tekrar deneme yapalım.
/tmp # apk add musl-dbg
fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/community/x86_64/APKINDEX.tar.gz
(1/1) Installing musl-dbg (1.2.4-r2)
OK: 545 MiB in 72 packages
/tmp # gdb -q main -c core-main.1496.e28334d67f07.1705227527
Reading symbols from main...
[New LWP 1496]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 __restore_sigs (set=set@entry=0x7ffe75a26320) at ./arch/x86_64/syscall_arch.h:40
40 ./arch/x86_64/syscall_arch.h: No such file or directory.
(gdb) bt
#0 __restore_sigs (set=set@entry=0x7ffe75a26320) at ./arch/x86_64/syscall_arch.h:40
#1 0x00007f1f223f8e5c in raise (sig=sig@entry=6) at src/signal/raise.c:11
#2 0x00007f1f223cbfa8 in abort () at src/exit/abort.c:11
#3 0x000056227738f1ff in mul (num=5, times=2) at main.c:9
#4 0x000056227738f251 in sub (num=10, sub=5) at main.c:18
#5 0x000056227738f29e in add (a=3, b=7) at main.c:25
#6 0x000056227738f2f3 in main (argc=2, argv=0x7ffe75a264e8) at main.c:31
Harika sonunda ilerleme kaydedebildik, aslında sorunun asıl sebebi bizim programdan değilmiş.
GDB debug sembolleri olmadığından musl
kütüphanesinin stack çözümlemesini yapamıyor ve o yüzden
bizim kodun da adımlarını gösteremiyor, musl
debug sembollerini ekleyince sorun çözüldü.
Musl debug sembollerini ekleyip sorunu çözdük deyip bıraksaydık işin eğlenceli kısmını, yani tavşan deliğinin derinliklerini keşfetme fırsatını kaçırırdık. Asıl eğlence bundan sonra başlıyor diyebiliriz.
Bu noktada LLDB’den biraz kopya çekmek istedim, en azından debug sembolleri olmadan bunu yapabildiğine göre ve işin içinde sihir olmadığına göre bir şekilde nasıl yaptığını öğrenip belki GDB’de aynı işi biz de yapabiliriz diye düşündüm. LLDB stack çözümleme işleminin nasıl yapıldığına dair logları isterseniz gösteriyor. Logları açıp tekrar aynı programı ve dump dosyasını yükledim sonuç aşağıdaki gibi oldu.
/tmp # lldb
(lldb) log enable lldb unwind
(lldb) target create main --core core-main.1496.e28334d67f07.1705227527
(x86_64) /tmp/main: Reading EH frame info
AssertFrameRecognizer::GetAbortLocation Unsupported OS
AssertFrameRecognizer::GetAbortLocation Unsupported OS
(x86_64) [vdso](0x00007ffe75b81000): Reading EH frame info
(x86_64) /lib/ld-musl-x86_64.so.1: Reading EH frame info
Process::SetPrivateState (stopped)
Process::SetPrivateState (stopped) stop_id = 1
th1/fr0 with pc value of 0x7f1f223f8d07, symbol name is '__setjmp'
th1/fr0 0x00007f1f223f8c91: CFA=rsp +8 => rsp=CFA+0 rip=[CFA-8]
th1/fr0 CFA is 0x7ffe75a26320: Register rsp (7) contents are 0x7ffe75a26318, offset is 8
th1/fr0 initialized frame current pc is 0x7f1f223f8d07 cfa is 0x7ffe75a26320 afa is 0xffffffffffffffff using assembly insn profiling UnwindPlan
th1/fr0 supplying caller's saved rip (16)'s location using assembly insn profiling UnwindPlan
th1/fr0 supplying caller's register rip (16) from the stack, saved at CFA plus offset -8 [saved at 0x7ffe75a26318]
th1/fr1 pc = 0x7f1f223f8e5c
th1/fr0 supplying caller's register rbp (6) from the live RegisterContext at frame 0
th1/fr1 fp = 0x7ffe75a26320
th1/fr0 supplying caller's saved rsp (7)'s location using assembly insn profiling UnwindPlan
th1/fr0 supplying caller's register rsp (7), value is CFA plus offset 0 [value is 0x7ffe75a26320]
th1/fr1 sp = 0x7ffe75a26320
th1/fr1 with pc value of 0x7f1f223f8e5c, symbol name is 'raise'
th1/fr1 Backing up the pc value of 0x7f1f223f8e5c by 1 and re-doing symbol lookup; old symbol was raise
th1/fr1 Symbol is now raise
th1/fr1 Using full unwind plan 'assembly insn profiling'
th1/fr1 active row: 0x00007f1f223f8e2a: CFA=rbp+160 => rbx=[CFA-24] rbp=[CFA-16] rsp=CFA+0 rip=[CFA-8]
...
...
...
Burada dikkatimi çeken ilk şey yukarıdaki loglarda geçen aşağıdaki satırlar oldu.
...
(x86_64) /lib/ld-musl-x86_64.so.1: Reading EH frame info
...
...
th1/fr0 initialized frame current pc is 0x7f1f223f8d07 cfa is 0x7ffe75a26320 afa is 0xffffffffffffffff using assembly insn profiling UnwindPlan
...
Yukarıdaki loglarda görüldüğü gibi LLDB çözümleme yaparken ilk olarak EH frame bilgisini okumuş ardından assembly insn profiling UnwindPlan
kullanıyorum demiş.
EH Frame daha önce duymadığım bir kavram olduğu için, tavşan deliğinden yeni bir tünele doğru yola çıkmış olduk.
EH Frame yani Exception Handling Frame C++
programlama dilinde exception oluşması durumunda hataya gelene kadar
tüm stack üzerinde bulunan tüm çağrı bilgilerinin çözümlenmesini kolaylaştırmak için DWARF
debug sembol standartının bir parçası olarak
geliştirilmiş. Kendisi, debug sembollerinin tutulduğu kısımda tutulmuyor, belirli bir tarihten beri derleyiciler siz aksini belirtmedikçe varsayılan olarak bu bilgiyi
derlenen binary
içerisine gömüyorlar. Amaç hata olduğunda ya da debug işlemi sırasında stack çözümlemeyi kolaylaştırmak. İşin güzel tarafı bu bilgi C
programları
için de aynı şekilde tutuluyor.
LLDB bu bilgiyi mi kullanıyor diye anlamak için önce, kullanılan musl-libc
kütüphanesinin .eh_frame
bilgisine bakalım.
/tmp # readelf --debug-dump=frames-interp /lib/ld-musl-x86_64.so.1
Contents of the .eh_frame section:
00000000 0000000000000014 00000000 CIE "zR" cf=1 df=-8 ra=16
LOC CFA ra
0000000000000000 rsp+8 c-8
00000018 0000000000000024 0000001c FDE cie=00000000 pc=0000000000014000..0000000000014070
LOC CFA ra
0000000000014000 rsp+16 c-8
0000000000014006 rsp+24 c-8
0000000000014010 exp c-8
00000040 0000000000000014 00000044 FDE cie=00000000 pc=00000000000140a0..000000000001432c
00000058 0000000000000014 0000005c FDE cie=00000000 pc=0000000000014330..00000000000145ed
00000070 0000000000000010 00000074 FDE cie=00000000 pc=00000000000145f0..00000000000149ee
Çıkan bilginin nasıl yorumlanacağına dair daha detaylı bilgileri referanslar kısmında bulabilirsiniz, fakat özetlemek gerekirse
bize pc
yani program counter
adresinin bulunduğu yere göre stack frame adresinin nerede bulunduğunu gösteren bir tablo oluşturmamıza olanak veriyor.
Örnek olarak aşağıdaki kısmını değerlendirelim
00000018 0000000000000024 0000001c FDE cie=00000000 pc=0000000000014000..0000000000014070
LOC CFA ra
0000000000014000 rsp+16 c-8
0000000000014006 rsp+24 c-8
0000000000014010 exp c-8
PC değeri 14000 ile 14070 arasında ise 14000 için CFA yani stack frame değeri, rsp+16
adresinde bulunur, ra
yani return address
değeri ise
CFA-8
adresinde bulunur gibi bilgiler vererek bu işi kolaylaştırıyor, stack çözümleme işlemi yaparken de bu tablo ile Call Stack
kolaylıkla çıkarılabiliyor.
Peki musl-libc
içinde bizim hata aldığımız yani setjmp
ya da raise
gibi fonksiyonlar bu tabloya dahil mi?
/tmp # objdump -S -Mintel /lib/ld-musl-x86_64.so.1
Yukarıdaki gibi disassemble ederek çıkan adresleri incelediğimde bizim hata aldığımız son iki fonksiyonun adreslerini aşağıdaki gibi görüyorum.
0000000000048c91 <__setjmp>:
48c91: 48 89 1f mov QWORD PTR [rdi],rbx
48c94: 48 89 6f 08 mov QWORD PTR [rdi+0x8],rbp
48c98: 4c 89 67 10 mov QWORD PTR [rdi+0x10],r12
48c9c: 4c 89 6f 18 mov QWORD PTR [rdi+0x18],r13
...
...
0000000000048e1c <raise>:
48e1c: 55 push rbp
48e1d: 53 push rbx
48e1e: 89 fb mov ebx,edi
48e20: 48 81 ec 88 00 00 00 sub rsp,0x88
48e27: 48 89 e5 mov rbp,rsp
...
...
Yukarıdaki adres bilgilerine (48c91,48e1c
) bakacak olursanız, bizim elimizde olan 14000-14070
aralığında değiller.
Buraya kadar oldukça umudum vardı, artık bu sorunun burada .eh_frame
tablosundaki bilgiler ile çözülebileceğini düşünmüştüm
ama maalesef, tavşan deliğinde önümüze çıkacak yeni tüneller var.
Yine de yukarıdaki .eh_frame
tablosunda neler var bakmak istedim, neden plt
yani Procedure Linkage Table
var hala emin değilim.
Disassembly of section .plt:
0000000000014000 <malloc@plt-0x10>:
14000: ff 35 2a 2f 08 00 push QWORD PTR [rip+0x82f2a] # 96f30 <stdout+0x1d8>
14006: ff 25 2c 2f 08 00 jmp QWORD PTR [rip+0x82f2c] # 96f38 <stdout+0x1e0>
1400c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000014010 <malloc@plt>:
14010: ff 25 2a 2f 08 00 jmp QWORD PTR [rip+0x82f2a] # 96f40 <malloc+0x71cb1>
14016: 68 00 00 00 00 push 0x0
1401b: e9 e0 ff ff ff jmp 14000 <malloc@plt-0x10>
...
...
Neden musl-libc
içinde sadece plt
kısmı ile ilgili bilgiler varken diğer kısımlar ile ilgili .eh_frame
bilgisi yok diye araştırırken
Musl mail listesinde bu konuya denk geldim orada, sebep aşağıdaki gibi gözüküyor
Accordingly, I would strongly prefer that Alpine try to solve this issue with
.debug_frame
first, and then we can look at.eh_frame
later if it winds up being insufficient for making basic debug functionality work (on at least the same level as glibc).
Konu içinde yazılmalara bakacak olursanız, backtrace
senaryosu için .eh_frame
bilgisinin içine konulmasını istemişler ama geliştiriciler
eğer debug yapmak istiyorsan musl-dbg
paketini yükle orada debug sembolleri var onunla işini hallet demişler özetle.
İki tarafı da bir noktaya kadar anlayabiliyorum, geliştiriciler çözüm sunmuş, aslında .eh_frame
çalışma zamanında
herhangi bir performans etkisi olmasa da, dağıtılan binary
boyutu belirli bir oranda büyüyor. Musl gibi gömülü ortamlarda çalışan, hatta Alpine
standart C kütüphanesi olarak seçilmesinin de en büyük sebebi güvenlik ve kompak oluşu, bundan dolayı eklememek onların açısından doğru.
Fakat kendi açımdan tamamen offline bir ortamda musl-dbg
paketini kuramadığım durumlar olduğu ve çok küçük bir boyut artışının bana ve çalıştığım projelere
zararı olmadığı için bu bilgi çıkarılmadan dağıtılması işime gelirdi.
Daha önce demiştik, derleyici herhangi bir ek parametre kullanmazsanız .eh_frame bilgisini otomatik olarak dahil ediyor. Peki bu tarz uç durumlarda biz bunu nasıl çıkarabiliriz, ya da Musl nasıl dahil etmemiş. Aslında derleyici bunları dahil etmek istemezseniz size yine seçenek sunuyor. Aşağıdaki parametreleri kullandığınızda .eh_frame olmadan çıktığını görebilirsiniz.
/tmp # gcc -fomit-frame-pointer -fno-exceptions -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -O0 main.c -o main2
/tmp # readelf --debug-dump=frames-interp main2
Contents of the .eh_frame section:
00000000 ZERO terminator
Contents of the .debug_frame section:
00000000 0000000000000014 ffffffff CIE "" cf=1 df=-8 ra=16
LOC CFA ra
0000000000000000 rsp+8 c-8
00000018 0000000000000014 00000000 FDE cie=00000000 pc=0000000000001096..00000000000010b9
/tmp # objdump -S -Mintel main2 | sed -n '/1096/,/10b9/p'
1091: e8 00 00 00 00 call 1096 <_start_c>
0000000000001096 <_start_c>:
1096: 8b 37 mov esi,DWORD PTR [rdi]
1098: 48 8d 57 08 lea rdx,[rdi+0x8]
109c: 4c 8d 05 8c 02 00 00 lea r8,[rip+0x28c] # 132f <_fini>
10a3: 45 31 c9 xor r9d,r9d
10a6: 48 8d 0d 53 ff ff ff lea rcx,[rip+0xffffffffffffff53] # 1000 <_init>
10ad: 48 8d 3d 03 02 00 00 lea rdi,[rip+0x203] # 12b7 <main>
10b4: e9 97 ff ff ff jmp 1050 <__libc_start_main@plt>
10b9: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
Aynı kodu yukarıdaki gibi derlediğimde .eh_frame bilgisi sanırım programın başlaması için gerekli olan _start_c
kısmı için konulmuş.
Diğer tüm kısımlarla ilgili bu bilgi kaldırılmış.
Sonuç olarak LLDB çözümlemeyi nasıl yapmış diye bakacak olursak, ilgili kısımlar için .eh_frame bilgisi olmadığına göre,
işin sırrı using assembly insn profiling UnwindPlan
satırında gizli diye düşünüyorum.
Biraz da LLDB tarafından GDB tarafına geçip logları inceleyelim, orada neden çözümleme yapamadığına dair ipucu bulabiliriz.
GDB ile aynı çalıştırılabilir dosyayı tekrar açtım. Oldukça fazla log üretiyor, çoğunu kırptım önemli olanlarını bıraktım.
Stack çözümleme loglarını açmak için set debug frame on
komutunu kullanıyoruz.
/tmp # gdb -q main -c core-main.1496.e28334d67f07.1705227527
Reading symbols from main...
warning: exec file is newer than core file.
[New LWP 1496]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f1f223f8d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) set debug frame on
[frame] get_prev_frame_always_1: enter
[frame] get_prev_frame_always_1: this_frame=-1
[frame] get_prev_frame_always_1: -> {level=0,type=NORMAL_FRAME,unwinder="amd64 epilogue",pc=0x7f1f223f8d07,id=<not computed>,func=<unknown>} // cached
[frame] get_prev_frame_always_1: exit
...
...
[frame] compute_frame_id: enter
[frame] compute_frame_id: fi=1
[frame] frame_unwind_find_by_frame: enter
[frame] frame_unwind_find_by_frame: this_frame=1
[frame] frame_unwind_arch: next_frame=0 -> i386:x86-64
[frame] frame_unwind_try_unwinder: trying unwinder "dummy"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "dwarf2 tailcall"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "inline"
[frame] frame_unwind_register_value: enter
[frame] frame_unwind_register_value: frame=0, regnum=16(rip)
[frame] frame_unwind_register_value: -> address=0x7ffe75a26318 lazy
[frame] frame_unwind_register_value: exit
[frame] frame_unwind_pc: this_frame=0 -> 0x7f1f223f8e5c
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "jit"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "python"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "amd64 epilogue"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "i386 epilogue"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "dwarf2"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "dwarf2 signal"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "amd64 sigtramp"
[frame] frame_unwind_try_unwinder: no
[frame] frame_unwind_try_unwinder: trying unwinder "amd64 prologue"
[frame] frame_unwind_try_unwinder: yes
[frame] frame_unwind_find_by_frame: exit
...
...
Önemli ilk log type=NORMAL_FRAME,unwinder="amd64 epilogue"
bu satır diye düşünüyorum. GDB ilk stack frame bilgisinin yani setjmp
kısmında kalan stack çağrısının normal bir frame olduğunu düşünüyor ve
amd64 epilogue
ile çözebileceğini sanıyor. Onun altında ise daha başka hangi stack çözümleme yöntemlerini kullanıyor ve sonuçları ne olmuş yazmış.
trying unwinder "dummy"
trying unwinder "dwarf2 tailcall"
trying unwinder "inline"
trying unwinder "jit"
trying unwinder "python"
trying unwinder "amd64 epilogue"
trying unwinder "i386 epilogue"
trying unwinder "dwarf2"
trying unwinder "dwarf2 signal"
trying unwinder "amd64 sigtramp"
trying unwinder "amd64 prologue"
Yukarıda listeden de görebileceğiniz gibi, elimizde musl-libc
için denenmiş çözümleme yöntemleri gözüküyor. Listede dwarf2
ile ilgili olan maddeler elimizde musl-libc
debug sembolleri
olmadığı için işe yaramıyor, diğerleri de direk derlediğimiz kodla alakalı değil, GDB’nin en mantıklı bulduğu ve seçtiği amd64 epilogue
tabanlı çözümleme yöntemi ise işe yaramıyor.
Buraya kadar GDB’nin neden düzgün analiz yapamayıp LLDB’nin yapabildiğini anladık diye düşünüyorum. LLDB yukarıda bahsettiğim gibi assembly
kodlarına bakarak bir çözümleme planı oluşturuyor
ve başarılı şekilde bize stack çağrılarını çıkarıyor. Yukarıda GDB’nin denediği maddeler arasında böyle bir çözümleme yöntemi yok ya da hata aldığı setjmp
fonksiyonu, GDB tarafından yanlış yorumlanıyor,
bundan dolayı ??
işaretlerini görüyoruz.
Bir sonraki bölümde bu sorunu nasıl GDB’ye yardımcı olarak giderebiliriz, kolları sıvayıp işe koyulacağız.
x86-64, assembly, call conventions
gibi alt seviye kavramları bilmemiz gerekiyor.
Bu yazı serisi 3 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara bu adresten ulaşabilirsiniz.
GDB üzerinden aynı core-dump dosyasını yükleyip aşağıdaki gibi disassemble
komutunu çalıştırınca bize, aşağıdaki gibi assembly
komutlarını gösteriyor.
Aşağıya sadece sub
ve mul
fonksiyonlarını görebilirsiniz.
(gdb) disassemble sub
Dump of assembler code for function sub:
0x000056227738f208 <+0>: push rbp
0x000056227738f209 <+1>: mov rbp,rsp
0x000056227738f20c <+4>: sub rsp,0x20
0x000056227738f210 <+8>: mov DWORD PTR [rbp-0x14],edi
...
...
0x000056227738f251 <+73>: leave
0x000056227738f252 <+74>: ret
End of assembler dump.
(gdb) disassemble mul
Dump of assembler code for function mul:
0x000056227738f1c5 <+0>: push rbp
0x000056227738f1c6 <+1>: mov rbp,rsp
0x000056227738f1c9 <+4>: sub rsp,0x10
0x000056227738f1cd <+8>: mov DWORD PTR [rbp-0x4],edi
...
...
0x000056227738f206 <+65>: leave
0x000056227738f207 <+66>: ret
Yukarıdaki iki fonksiyonun ilk ve son iki satırı dikkatinizi çekmiştir, satırlar aynı şekilde başlıyor.
push rbp
mov rbp,rsp
...
...
leave
ret
Basitçe açıklamak gerekirse, kodunuz içerisinde her fonksiyon çağrısı yapıldığında, stack üzerinde bir Call Frame oluşur.
Bunun amacı her fonksiyonun kendi içinde kullandığı lokal değişkenleri tutmak, çalıştıktan sonra da onu çağıran fonksiyon adresine
geri dönmek olarak özetlenebilir. Yani her fonksiyon çağrısı için bir stack/call frame
oluşur, bunu oluşturmak için de, yukarıdaki gibi
ilk başta standart bir prologue
kodu, fonksiyon bittiğinde de epilogue
assembly kodu konularak bir önceki fonksiyona dönüş sağlanır.
Fonksiyon çağrıları devam ettikçe stack aşağıdaki gibi gözükür.
Bu adreslerdeki değerlere bakınca aslında bir, stack çözümleme bağlı liste veri yapısı oluşturmak kadar kolay. Her stack çerçevesi için
rbp
değerini bul, sonra geriye doğru bunları birleştir ve çözümlemeyi bitir.
Stack çözümleme yaparken x86-64 mimarisinde kullanılan çağrı standartlarını da bilmek gerekiyor. CPU üzerinde değerleri tutabileceğimiz sınırlı
sayıda register
bulunuyor. Haliyle, bir fonksiyon içinde işlem yaparken bazı register değerleri kullanıldıktan sonra başka bir fonksiyon çağrılabilir.
Bu aşamada diğer fonksiyonun, ezebileceği ve kendi istediği gibi kullanabileceği register isimleri olduğu var. Ayrıca sizin değerlerini ezmeden, önce
kaydedip daha sonra bu değerleri ilk değerlerine döndürmeniz gereken register değerleri de bulunuyor.
Callee-saved
yani çağrılan fonksiyonun koruması gereken register isimleri aşağıdaki gibi. Daha detaylı bilgiyi buradan
ulaşabilirsiniz.
Register | Convention |
---|---|
rsp | Stack pointer, callee-saved |
rbx | Local variable, callee-saved |
rbp | Local variable, callee-saved |
r12 | Local variable, callee-saved |
r13 | Local variable, callee-saved |
r14 | Local variable, callee-saved |
r15 | Local variable, callee-saved |
İşimiz bu kadar kolay demek isterdim ama değil, çünkü optimizasyonlar işin içine girdiğinde standart prologue
ve epilogue
düşündüğümüz
gibi olmuyor. Örnek olarak -O1 optimizasyonlarını açıp örnek kodumuz tekrar derleyelim ve neler değişiyor bakalım.
/tmp # gcc -Wall -O1 main.c -o main3
/tmp # gdb -q main3
Reading symbols from main3...
(gdb) disassemble sub
Dump of assembler code for function sub:
0x00000000000011fe <+0>: push %rbp
0x00000000000011ff <+1>: push %rbx
0x0000000000001200 <+2>: sub $0x8,%rsp
...
...
0x000000000000122f <+49>: pop %rbx
0x0000000000001230 <+50>: pop %rbp
0x0000000000001231 <+51>: ret
End of assembler dump.
(gdb) disassemble mul
Dump of assembler code for function mul:
0x00000000000011c5 <+0>: push %rbp
0x00000000000011c6 <+1>: push %rbx
0x00000000000011c7 <+2>: sub $0x8,%rsp
...
...
0x00000000000011f6 <+49>: pop %rbx
0x00000000000011f7 <+50>: pop %rbp
0x00000000000011f8 <+51>: ret
0x00000000000011f9 <+52>: call 0x1030 <abort@plt>
End of assembler dump.
(gdb) break sub
Breakpoint 1 at 0x11fe
(gdb) run 15
Starting program: /tmp/main3 15
Number: 15
Adding numbers: 3,7
Breakpoint 1, 0x00005555555551fe in sub ()
(gdb) p $rbp
$1 = (void *) 0x7
(gdb)
Yukarıda önce ilk seviye optimizasyonları açıp tekrar derledik, ardından kodu disassemble ettik. İlk ve son iki satırlardaki değişikliği fark ettiniz diye düşünüyorum.
Klasik prologue
ve epilogue
komutları artık yok, hatta rbp
değerinde stack base adresi değil de, başka değer tutulduğunu göstermek için sub
metoduna breakpoint
koyup tekrar çalıştırdım ve tutulan değerin 0x7
yani bizim, main
içerisinde gönderdiğimiz parametrelerden birisi. Optimizasyonlar olmadan normalde stack frame
adresini tutmak için kullanılan rbp
, artık fonksiyona geçilen parametre değerini tutmak için kullanılmış. Bu optimizasyonun adı fomit-frame-pointer
olarak geçiyor,
aşağıda da giriş seviye optimizasyonlar da bile enable
edildiğini görebilirsiniz.
/tmp # gcc -Q -O2 --help=optimizers | grep frame
-fomit-frame-pointer [enabled]
Bu optimizasyonun ana sebebi aslında performans artışı, rbp
boşa çıkıp, parametre geçme gibi diğer amaçlarla kullanıldığında hem elimizde ekstra bir register
oluyor,
hem de prologue ve epilogue işlemlerinde rbp değerini stack üzerine kaydedip geri almak için oluşturulan makine komutları ortadan kalkıyor.
Sonuç olarak sadece bu optimizasyon sayesinde ortalama %10-15 performans artışı sağladığı raporlanıyor.
Elimizdeki rbp
base stack frame adresini göstermediğinde stack frame ortadan kalkmıyor aslında. Tamam rbp
baz alınarak kolayca
frame adresi bulanamaz ama frame yine orada, sadece bu sefer rsp
değerine göre bu hesaplama yapılabilir.
Çalışan kodun assembly
komutlarını görebiliyoruz, bu komutlardan hangisinin rsp
diğerlerini değiştirdiğini de assembly
koduna bakarak hesaplayabiliriz.
Bu hesaplama sonrasında o fonksiyon içinde stack nereden başlar, bir önceki fonksiyon adresi nerededir diye bulabiliriz, çünkü bunlar hala stack üzerinde tutulan
değerler.
İlk olarak bu adımdan, yani core dump alındığında bellekte çalışan, en alttaki stack frame fonksiyonundan başlayalım. Bunu çözümledikten sonra, adım adım bir üstte bulunan fonksiyon çağrılarına giderek devam edeceğiz.
Aşağıdaki gibi GDB’yi başlatarak disassemble
diyerek kodu gördük.
/tmp # gdb -q main -c core-main.1496.e28334d67f07.1705227527
Reading symbols from main...
[New LWP 1496]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f1f223f8d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) disassemble
Dump of assembler code for function setjmp:
0x00007fe04dc70c91 <+0>: mov %rbx,(%rdi)
0x00007fe04dc70c94 <+3>: mov %rbp,0x8(%rdi)
0x00007fe04dc70c98 <+7>: mov %r12,0x10(%rdi)
0x00007fe04dc70c9c <+11>: mov %r13,0x18(%rdi)
0x00007fe04dc70ca0 <+15>: mov %r14,0x20(%rdi)
0x00007fe04dc70ca4 <+19>: mov %r15,0x28(%rdi)
0x00007fe04dc70ca8 <+23>: lea 0x8(%rsp),%rdx
0x00007fe04dc70cad <+28>: mov %rdx,0x30(%rdi)
0x00007fe04dc70cb1 <+32>: mov (%rsp),%rdx
0x00007fe04dc70cb5 <+36>: mov %rdx,0x38(%rdi)
0x00007fe04dc70cb9 <+40>: xor %eax,%eax
0x00007fe04dc70cbb <+42>: ret
0x00007fe04dc70cbc <+43>: mov %rdi,%rdx
0x00007fe04dc70cbf <+46>: mov $0x8,%r10d
0x00007fe04dc70cc5 <+52>: lea 0x4aafc(%rip),%rsi # 0x7fe04dcbb7c8
0x00007fe04dc70ccc <+59>: xor %edi,%edi
0x00007fe04dc70cce <+61>: mov $0xe,%eax
0x00007fe04dc70cd3 <+66>: syscall
0x00007fe04dc70cd5 <+68>: ret
0x00007fe04dc70cd6 <+69>: mov %rdi,%rdx
0x00007fe04dc70cd9 <+72>: mov $0x8,%r10d
0x00007fe04dc70cdf <+78>: lea 0x4aada(%rip),%rsi # 0x7fe04dcbb7c0
0x00007fe04dc70ce6 <+85>: xor %edi,%edi
0x00007fe04dc70ce8 <+87>: mov $0xe,%eax
0x00007fe04dc70ced <+92>: syscall
0x00007fe04dc70cef <+94>: ret
0x00007fe04dc70cf0 <+95>: mov %rdi,%rsi
0x00007fe04dc70cf3 <+98>: mov $0x8,%r10d
0x00007fe04dc70cf9 <+104>: mov $0xe,%eax
0x00007fe04dc70cfe <+109>: xor %edx,%edx
0x00007fe04dc70d00 <+111>: mov $0x2,%edi
0x00007fe04dc70d05 <+116>: syscall
=> 0x00007fe04dc70d07 <+118>: ret
End of assembler dump.
Yukarıdaki kodu biraz incelersek, rsp
değerini değiştiren push,pop,add,sub
gibi assembly komutları bulunmuyor.
Bu da bize şunu gösteriyor, bu fonksiyon call
ile çağrıldığına göre, bir önceki fonksiyon adresi stack üzerinde bulunuyor. Call
çağrısının aslında şu şekilde uzun olarak yazılabileceğini düşünebiliriz.
push return_address
jmp function_address
Yani call
yaptığında bizim bir önceki fonksiyonda kaldığımız yer, stack içine push
ile kaydedildi. Ondan sonra da stack değerini değiştiren bir
komut olmadığına göre demek ki bizim bir önceki fonksiyonun adresi şimdiki rsp
değerinin gösterdiği adres değeri diyebiliriz.
Ayrıca bir önceki fonksiyonda en son rsp
değerini de, rsp+8
olarak hesaplayabiliriz. GDB üzerinde yapalım işlemi ve sonuca bakalım.
(gdb) info symbol *(void**)$rsp
raise + 64 in section .text of /lib/ld-musl-x86_64.so.1
Yukarıdaki komutlar ile stack üzerinde rsp
gösterdiği yerin fonksiyon bilgisini aldık, ve raise + 64
olduğunu öğrendik.
Return address değerimiz belirlendi, diğer değer stack frame adresini de, yukarıda konuştuk. Kodlar içinde stack pointer değerini
değiştiren bir şey olmadığından stack frame adresimiz rsp
ile aynı. Çözümleme sonuçları aşağıdaki gibi oldu.
No | Function | Caller Return Address | Caller Stack Pointer |
---|---|---|---|
0 | setjmp | *(void**)(rsp) | rsp+8 |
İlk frame içinde, bir önceki bizi çağıran fonksiyonun raise
olduğunu bulmuştuk. Şimdi bu fonksiyonu inceleyelim
ve stack frame adresini bulmaya çalışalım.
(gdb) disassemble raise
Dump of assembler code for function raise:
0x00007fe04dc70e1c <+0>: push %rbp
0x00007fe04dc70e1d <+1>: push %rbx
0x00007fe04dc70e1e <+2>: mov %edi,%ebx
0x00007fe04dc70e20 <+4>: sub $0x88,%rsp
0x00007fe04dc70e27 <+11>: mov %rsp,%rbp
0x00007fe04dc70e2a <+14>: mov %rbp,%rdi
0x00007fe04dc70e2d <+17>: call 0x7fe04dc70cd6 <setjmp+69>
0x00007fe04dc70e32 <+22>: movslq %ebx,%rsi
0x00007fe04dc70e35 <+25>: mov %fs:0x0,%rax
0x00007fe04dc70e3e <+34>: movslq 0x30(%rax),%rdi
0x00007fe04dc70e42 <+38>: mov $0xc8,%eax
0x00007fe04dc70e47 <+43>: syscall
0x00007fe04dc70e49 <+45>: mov %rax,%rdi
0x00007fe04dc70e4c <+48>: call 0x7fe04dc45e45 <fetestexcept+6160>
0x00007fe04dc70e51 <+53>: mov %rbp,%rdi
0x00007fe04dc70e54 <+56>: mov %rax,%rbx
0x00007fe04dc70e57 <+59>: call 0x7fe04dc70cf0 <setjmp+95>
=> 0x00007fe04dc70e5c <+64>: add $0x88,%rsp
0x00007fe04dc70e63 <+71>: mov %ebx,%eax
0x00007fe04dc70e65 <+73>: pop %rbx
0x00007fe04dc70e66 <+74>: pop %rbp
0x00007fe04dc70e67 <+75>: ret
End of assembler dump.
Yukarıdaki koda baktığımızda, stack değerini değiştiren push,sub
gibi komutlar bulunuyor. Stack yüksek bellek adresinden
başlayıp, düşük bellek adresine doğru genişler. sub
gibi komutlar ise, o stack frame içinde lokal değişkenlere yer açar.
İlk olarak 2 adet push
işlemi yapılmış, sonra gördüğünüz gibi 0x88
boyutunda yer açılmış
(gdb) info symbol *(void**)($rsp+0x88+16)
abort + 14 in section .text of /lib/ld-musl-x86_64.so.1
Ayrıca optimize edilmiş bir kod olduğu için, ilk gördüğünüz push rbp, stack base pointer değerini kaydetmiyor. Fonksiyona parametre olarak geçilen argümanı kaydettiği için, o değeri stack base pointer değeri olarak alamıyoruz ve bu hesaplamaları kendimiz yapıyoruz. Bu hesaplamadan sonra tablomuz aşağıdaki gibi oldu.
No | Function | Caller Return Address | Caller Stack Pointer |
---|---|---|---|
0 | setjmp | *(void**)(rsp) | rsp+8 |
1 | raise | *(void**)(rsp+0x88+16) | rsp+0x88+16+8 |
Bir önceki adımda raise
fonksiyonunu çağıran fonksiyonun abort
olduğunu belirlemiştik, şimdi abort için stack nerede başlar nerede biter ve onu çağıran fonksiyonun
frame değerlerini çıkaralım.
(gdb) disassemble abort
Dump of assembler code for function abort:
0x00007fe04dc43f9a <+0>: sub $0x38,%rsp
0x00007fe04dc43f9e <+4>: mov $0x6,%edi
0x00007fe04dc43fa3 <+9>: call 0x7fe04dc70e1c <raise>
0x00007fe04dc43fa8 <+14>: xor %edi,%edi
0x00007fe04dc43faa <+16>: call 0x7fe04dc70cbc <setjmp+43>
0x00007fe04dc43faf <+21>: lea 0x7e006(%rip),%rdi # 0x7fe04dcc1fbc
0x00007fe04dc43fb6 <+28>: call 0x7fe04dc7c51f
0x00007fe04dc43fbb <+33>: mov $0x8,%edx
0x00007fe04dc43fc0 <+38>: xor %eax,%eax
0x00007fe04dc43fc2 <+40>: lea 0x10(%rsp),%rdi
0x00007fe04dc43fc7 <+45>: mov %rdx,%rcx
0x00007fe04dc43fca <+48>: mov $0x6,%r8d
0x00007fe04dc43fd0 <+54>: lea 0x10(%rsp),%rsi
0x00007fe04dc43fd5 <+59>: mov $0x8,%r10d
0x00007fe04dc43fdb <+65>: rep stos %eax,%es:(%rdi)
0x00007fe04dc43fdd <+67>: mov $0xd,%eax
0x00007fe04dc43fe2 <+72>: mov %r8,%rdi
0x00007fe04dc43fe5 <+75>: mov %rcx,%rdx
0x00007fe04dc43fe8 <+78>: syscall
0x00007fe04dc43fea <+80>: mov %fs:0x0,%rax
0x00007fe04dc43ff3 <+89>: mov %r8,%rsi
0x00007fe04dc43ff6 <+92>: movslq 0x30(%rax),%rdi
0x00007fe04dc43ffa <+96>: mov $0xc8,%eax
0x00007fe04dc43fff <+101>: syscall
0x00007fe04dc44001 <+103>: mov $0xe,%eax
0x00007fe04dc44006 <+108>: lea 0x8(%rsp),%rsi
0x00007fe04dc4400b <+113>: mov $0x1,%edi
0x00007fe04dc44010 <+118>: movq $0x20,0x8(%rsp)
0x00007fe04dc44019 <+127>: syscall
0x00007fe04dc4401b <+129>: hlt
0x00007fe04dc4401c <+130>: mov $0x9,%edi
0x00007fe04dc44021 <+135>: call 0x7fe04dc70e1c <raise>
0x00007fe04dc44026 <+140>: mov $0x7f,%edi
0x00007fe04dc4402b <+145>: call 0x7fe04dc43f84 <_Exit>
End of assembler dump.
abort
metodunu incelediğimizde, stack üzerinde 0x38
kadar yer açmış, bunun dışında başka bir şey koymamış. Buna bakarak
bir önceki bizi çağıran fonksiyonun adresi, rsp+0x38
adresinde diyebiliriz. Tabi bu değeri raise içinden hesaplayabiliriz, çünkü GDB
abort fonksiyonunu geçerli bir frame olarak görmediği için şöyle yapmamız gerekecek.
(gdb) frame 1
#1 0x00007fe04dc70e5c in raise () from /lib/ld-musl-x86_64.so.1
(gdb) info symbol *(void**)($rsp+0x88+16+8+0x38)
mul + 58 in section .text of /tmp/main
rsp+0x88+16
bu değer zaten raise içinde abort fonksiyonun dönüş adres değeriydi, onun üzerine +8
eklediğimizde abort
içinde kaydedilmiş son rsp
değerini bulduk
sonra da abort
içinde sub $0x38,%rsp
komutundan dolayı 0x38
ekledik. Bu da bize bir önce bizi çağıran fonksiyonun adını mul
olarak gösterdi.
Tablomuzun son hali aşağıdaki gibi oldu.
No | Function | Caller Return Address | Caller Stack Pointer |
---|---|---|---|
0 | setjmp | *(void**)(rsp) | rsp |
1 | raise | *(void**)(rsp+0x88+16) | rsp+0x88+16+8 |
2 | abort | *(void**)(rsp+0x38) | rsp+0x38+8 |
Bu aşamadan sonra devam edip, kendi yazdığımız kodun fonksiyonlarına kadar çıkmak mümkün açıkçası. Ama GDB bundan sonra sorunlu yeri geçtikten sonra diğer stack frame bilgilerini kendisi çıkarabiliyor. Bunu sürekli el ile yapmak tabi mümkün değil ama otomasyon haline getirmek için adım adım nasıl yapıldığını bilmek gerekiyordu. Şimdi bir sonra ki bölümde bunu nasıl otomasyon haline getirebiliriz onu inceleyelim.
Bu yazı serisi 3 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara bu adresten ulaşabilirsiniz.
GDB bu tarz otomasyon gerektiren durumlar için batch mode
ya da GDB Script
denilen basit bir betik dil seçeneği sunuyor.
Ama daha önce Python
desteği sunduğunu bilmiyordum. Oldukça gelişmiş bir API desteği sunuyor.
Bunun sayesinde biz de debug sembolleri olmayan musl-libc
stack frame çözümlemesi yapan bir eklenti yazalım.
Yazdığım eklenti aslında, bir önceki bölümde anlattığım adımları otomatik yapıyor, yani önce şuanda çalışan
kodun musl
kütüphanesine ait bir frame olup olmadığı kontrol ediliyor. Eğer evet ise disassemble
ediyor,
kodun içinde geçen push,sub
gibi değerleri sayıp stack frame içinde ne kadar yer açılmış onu buluyor.
Bunu bulduktan sonra zaten bir önceki fonksiyonun return address
değerine ulaşmış oluyoruz.
import re
import gdb
from gdb.unwinder import Unwinder
def debug(pc, current_rsp, offset, addr, frame_id, func):
print('=============debug===========')
print('{:<20}:{:<8}'.format('function',func))
print('{:<20}:{:<8}'.format('pc', str(pc)))
print('{:<20}:{:<8}'.format('current_rsp', str(current_rsp)))
print('{:<20}:{:<8}'.format('offset', str(offset)))
print('{:<20}:{:<8}'.format('return address', hex(addr)))
print('{:<20}:{:<8}'.format('frame_id', str(frame_id)))
u64_ptr = gdb.lookup_type('unsigned long long').pointer()
class FrameID:
def __init__(self, sp, pc):
self.sp = sp
self.pc = pc
def __str__(self):
return f'sp: {self.sp}, pc: {self.pc}'
class MuslUnwinder(Unwinder):
def __init__(self):
super().__init__("musl_unwinder")
def is_musl_frame(self,pc):
obj = gdb.execute("info symbol 0x%x" % pc, False, True)
return "musl" in obj
def dereference(self,adr):
deref = gdb.parse_and_eval("0x%x" % adr).cast(u64_ptr).dereference()
return deref
def __call__(self, pending_frame):
frame = pending_frame.level()
pc = pending_frame.read_register("pc")
if not self.is_musl_frame(pc):
return None
asm = gdb.execute("disassemble 0x%x" % pc, False, True)
lines = asm.splitlines()
func = None
args_bytes = 0
locals_bytes = 0
rbp_bytes = 0
for line in lines:
m = re.match('Dump of assembler code for function (.*):', line)
if m:
func = m.group(1)
elif re.match('.*push[ ]*%', line):
args_bytes += 8
if "rbp" in line:
rbp_bytes += 8
elif m := re.match('.*sub[ ]*\\$0x([A-Fa-f0-9]+),%rsp', line):
locals_bytes = int(m.group(1), 16)
break
offset = locals_bytes + args_bytes
current_rsp = pending_frame.read_register("rsp")
current_rbp = pending_frame.read_register("rbp")
rsp = current_rsp + offset + 8
return_addr = self.dereference(current_rsp + offset)
frame_id = FrameID(rsp, pc)
unwind_info = pending_frame.create_unwind_info(frame_id)
unwind_info.add_saved_register("rsp", rsp)
unwind_info.add_saved_register("rip", return_addr)
if rbp_bytes > 0:
saved_rbp = self.dereference(current_rsp+locals_bytes+rbp_bytes)
unwind_info.add_saved_register("rbp", saved_rbp)
else:
unwind_info.add_saved_register("rbp", current_rbp)
if gdb.parameter("verbose"):
debug(pc, current_rsp, offset, return_addr, frame_id, func)
return unwind_info
gdb.execute('set disassembly-flavor att')
gdb.unwinder.register_unwinder(None, MuslUnwinder(), replace=True)
gdb.invalidate_cached_frames()
Hatırlarsanız Caller ve Callee Saved Registers
başlığı altında bazı register
değerlerinin çağrılan fonksiyon tarafından korunması ve eski haline geri
döndürülmesi gerektiğini söylemiştik. Eğer kodun içinde rbp
değeri stack üzerine kaydedildiyse, onu da önceki frame için
add_saved_register
olarak kaydediyoruz. Bunu eklemediğim zaman çözümleme bazı üst stack frame çözümlemeleri hata verebiliyor.
Diğer kaydedilmesi gereken register değerleri için bir şey yapmadım, muhtemelen bu kod başka bir kütüphane için kullanılırsa, eklemek ya da değiştirmek gerekebilir.
Evet artık sona doğru yaklaşıyoruz, yukarıdaki kodu muslunwinder.py
olarak kaydetmiştim.
Eski core dump dosyasını tekrar açıyorum, önce unwinder
olmadan backtrace
almaya çalıştım.
/tmp # gdb -q main -c core-main.2041.e28334d67f07.1705390818
Reading symbols from main...
[New LWP 2041]
Core was generated by `./main 15'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007fe04dc70d07 in setjmp () from /lib/ld-musl-x86_64.so.1
(gdb) bt
#0 0x00007fe04dc70d07 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007fe04dc70e5c in raise () from /lib/ld-musl-x86_64.so.1
#2 0x0000003000000008 in ?? ()
#3 0x00007ffc5622a1b0 in ?? ()
#4 0x00007ffc5622a0f0 in ?? ()
#5 0x00007ffc5622a220 in ?? ()
#6 0x0000000000000005 in ?? ()
#7 0x0000000000000002 in ?? ()
#8 0x0000000000000000 in ?? ()
Şimdi eklentiyi yükleyip tekrar deneyelim.
(gdb) source muslunwinder.py
(gdb) bt
#0 0x00007fe04dc70d07 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007fe04dc70e5c in raise () from /lib/ld-musl-x86_64.so.1
#2 0x00007fe04dc43fa8 in abort () from /lib/ld-musl-x86_64.so.1
#3 0x000055904dae41ff in mul ()
#4 0x000055904dae4251 in sub ()
#5 0x000055904dae429e in add ()
#6 0x000055904dae42f3 in main ()
Şimdi bir de, kendi yazdığımız kod değil, ilk bölümde bahsettiğim NodeJS dump dosyası üzerinde deneyelim.
/tmp # gdb /usr/local/bin/node -c core.f8f32091796c.node.1700129772.28 -q
Reading symbols from /usr/local/bin/node...
[New LWP 28]
[New LWP 30]
[New LWP 29]
[New LWP 33]
[New LWP 31]
[New LWP 32]
[New LWP 34]
Core was generated by `/usr/local/bin/node template.js'.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007fa1a8f5a3f2 in setjmp () from /lib/ld-musl-x86_64.so.1
[Current thread is 1 (LWP 28)]
(gdb) bt
#0 0x00007fa1a8f5a3f2 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007fa1a8f5a54d in raise () from /lib/ld-musl-x86_64.so.1
#2 0x00007fa1a8f5b9a9 in ?? () from /lib/ld-musl-x86_64.so.1
#3 0x00007fa1a8faae98 in ?? () from /lib/ld-musl-x86_64.so.1
#4 0x0000000000000000 in ?? ()
(gdb) source muslunwinder.py
(gdb) bt
#0 0x00007fa1a8f5a3f2 in setjmp () from /lib/ld-musl-x86_64.so.1
#1 0x00007fa1a8f5a54d in raise () from /lib/ld-musl-x86_64.so.1
#2 0x00007fa1a8f30f25 in abort () from /lib/ld-musl-x86_64.so.1
#3 0x00005641a6ef5e55 in node::Abort() ()
#4 0x00005641a6e00d27 in node::OnFatalError(char const*, char const*) ()
#5 0x00005641a70ed0e2 in v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) ()
#6 0x00005641a70ed46f in v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) ()
#7 0x00005641a72bf365 in v8::internal::Heap::FatalProcessOutOfMemory(char const*) ()
...
...
Evet gizemli ??
işaretleri yerine artık anlamlı fonksiyon isimleri görebiliyoruz, hem de debug sembollerini yüklemeden.
Tavşan deliğinde tünelin ucundaki ışık gözüktü. Bol hesaplamalı, saç baş yolmalı bir, yolculuk olsa da benim için oldukça
eğlenceli ve öğretici oldu diyebilirim.
It’s not DNS
There’s no way it’s DNS
It was DNS
Son zamanlarda müşteri ortamlarında yaşadığımız problemler dönüp dolanıp DNS
‘e çıkmaya
başladı. Durum böyle olunca sorunları ve kullandığımız teknolojilerin, davranışlarını incelemek şart oldu.
Konuya uygun bir başlık ararken, sorun hep DNS
gibi bir arama yaptığımda yukarıdaki alıntı ile ilk defa karşılaştım.
DNS
ile ilgili benzer sorunları yaşayan bir arkadaş , sistem yöneticileri için Reddit
üzerinde sanırım bu konuda bir
kartpostal hazırlamış ve bu da oldukça meşhur olmuş. Birçok insan benzer sorunları yaşadığı için tabi alıntı almış başını yürümüş
Kullandığımız web siteleri, geliştirdiğimiz platformlar, kullandığımız işletim sistemleri, çağırdığımız API yöntemleri, en basitinden IP adresi yerine, genellikle isimlerle çalışıyor. Ama modern internet altyapısı IP protokolü üzerinden çalıştığı için verilen ismin önce IP adresi çözüldükten sonra ilgili port üzerinden, bir protokol aracılığı ile geri kalan işlemler yapılıyor. IP adresini öğrenemediğiniz durumda kalan işlemler doğal olarak gerçekleştirilemiyor.
Uygulama geliştirenler için DNS
önemli mi? Cevap maalesef evet. Eğer bir web, network, veri tabanı programlama gibi bir alanda bir şeyler geliştiriyorsanız,
eninde sonunda elinizde olan ismi, adresi IP adresine dönüştürüp işleme devam etmek zorundasınız. Örneğin NodeJS, Python
gibi bir platformda bir HTTP Web Request
oluşturup REST API
çağırdınız, eğer IP kullanmıyorsanız, önce DNS protokolü ile IP adresi çözümlenecek ardından sizin işleminiz devam edecek.
Kısacası DNS
Client-Server yapısında bir protokol olduğu için belki bir sistem yöneticisi gibi DNS Server kurup yönetmeyeceksiniz ama geliştirdiğiniz yazılımlarda
belki siz farketmeseniz de, kullandığınız programlama dili ya da işletim sistemi aracılığı ile her zaman DNS Client olarak resmin içinde olacaksınız.
Web geliştirici olara çalışıyorsunuz ve aşağıdaki gibi bir kod yazdınız.
http.get({
hostname: 'myapi.internal.com',
port: 443,
path: '/test',
}, (res) => {
console.log(res);
});
Bu isteğin gerçekleştirilmesi için önce, ismin çözülüp IP adresinin elde edilmesi gerekiyor. Tabi NodeJS
bunu kendi başına yapmıyor, öncelikle
çalıştığı işletim sistemi üzerinden isim çözme ile ilgili sunular fonksiyonları kullanarak bunu yapıyor ve ardından işleme devam ediyor.
İşletim sisteminin ilgili fonksiyonlar deyince, tabi çekirdek olarak farklı fonksiyonlara, farklı soket katmanlarına sahip olsalar da, NodeJS, Python
gibi
platformlar işletim sistemlerinin sistem çağrılarını kullanmak yerine arada soyutlama katmanı olarak kullanılan genellikle C Standard Library gibi kütüphaneleri kullanarak bu işlemleri yapıyorlar.
Bu arada, işletim sistemi sistem çağrıları kullanılarak da yapılabilir ama platform geliştirenler için standardı kullanmak geliştirme ve bakım maliyeti için her zaman avantajlı olduğundan, ve standard olarak işletim sistemleri tarafından C kütüphanesi sunulduğundan, dönüp dolanıp bir türlü kurtulamadığımız C programlama diline ya da kütüphanelerine ayağımız yine dolaşıyor.
FreeBSD, NetBSD
gibi POSIX işletim sistemlerinde LibC çekirdeğin bir parçası olsa da, Linux dağıtımlarında çekirdeğin dışında ve kullanıcı tarafı(user space) bir
kütüphane olarak yükleniyor. Dolayısıyla farklı Linux dağıtımları farklı LibC kütüphanelerine sahip olabiliyor. Tabi standart C kütüphanesinin sağlaması gereken
fonksiyonlar aynı olsa da, çalışma mantıkları farklı olabiliyor, hatta ek özellikler sunabiliyorlar. Yazılımı etkileyen en önemli kısım ise, yöntem farklılığı
geliştirdiğiniz yazılımları etkileyebiliyor. Bu linkten farklı standart C kütüphanelerinin detaylı karşılaştırmalarına bakabilirsiniz.
Buraya kadar uzun bir giriş oldu farkındayım, şimdi asıl konumuza dönelim. Artık, Docker, Kubernetes
gibi container platformlar hayatımızın parçası.
Geliştirdiğimiz ürünler, servisler bu platformlar üzerinden yayınlanıyor. Tabi artık servis sayıları fazla, ve DNS protokolü de hem dış
dünya hem de, servisler arasında iletişimin en önemli parçalarından birisi.
Belki çoğumuz , container platformları işin için de olmasa, Musl Libc ile pek işimiz olmayacak. Ama özellikle biz de birçok servisimizde oldukça küçük boyuta sahip, güvenlik odaklı Alpine Linux tabanlı imajları kullanıyoruz. Bu Linux dağıtımı yine benzer sebeplerle standart C kütüphanesi olarak Musl kullanıyor. Yukarıda bahsettiğim, DNS çözümleme işlemi standart C kütüphanesinin sorumluluğunda olduğu için bunun hangi mantıkta, nasıl yapıldığı uygulamalarımızı etkileyebiliyor. Peki Musl isim çözümleme işlemini nasıl yapıyor?
Linux/Unix işletim sistemlerinde standart DNS konfigürasyonu resolv.conf
dosyası aracılığı ile yapılıyor. Standart C kütüphaneleri de bu dosyayı okuyarak içinde bulunan
nameserver
ve diğer değerlere göre IP çözümleme işlemini yapıyorlar. Bu dosyayı tamamen yok sayıp bu işlem yapılabilir ama standartlardan konuştuğumuzu tekrar hatırlayalım.
Örnek olarak bu dosyada şöyle bir konfigürasyona sahip olduğumuzu düşünelim.
options edns0 trust-ad
nameserver 8.8.8.8 #Global DNS
nameserver 8.8.4.4 #Global DNS
nameserver 192.168.100.20 #Local DNS
Benim daha önceki kafa karışıklığım şundan kaynaklanıyordu. Buraya yazılan DNS server adreslerinin hepsinde sırayla arama işlemi yapılıp bulduğu adresi cevap olarak dönüyor sanıyordum.
Fakat o mantıkla çalışmıyor, burada olan adreslerin hepsi birbirinin yedeği gibi düşünülüyor, yani hepsinin size normalde verilen isim için aynı IP adresini çözeceği varsayılıyor.
Eğer yukarıdaki server IP adreslerinden birine ulaşımda hata alırsa, diğerlerinden gelen cevabı kabul ediyor, ama ulaşım varsa hangisinden cevap gelirse gelsin, doğru olarak kabul ediliyor.
Benim gibi, global DNS sunucularını yukarıya, local DNS sunucularını aşağıya ,yani birbirinin yedeği olmayan DNS sunucuları aynı resolv.conf
için hangi sırayla yazarsanız yazın beklemediğiniz sonuçlarla karşılaşabilirsiniz.
Burada C kütüphaneleri içinde işleyiş de birbirinden farklı. Örnek olarak resolv.conf
içeriği yukarıdaki gibi olan ve Musl kullanan bir Alpine container içinde bir CLI komutu ve basit bir NodeJS kodu ile aşağıdaki gibi DNS sorgusu yapıyorum.
nslookup
ya da dig
gibi araçları özellikle kullanmadım, çünkü onlar bu iş için tasarlandığından dolayı resolv.conf konfigürasyonunu ya da standart C kütüphanesinin sağladığı fonksiyonları
yok sayabiliyorlar.
bash-5.1# getent hosts adc.example.local
192.168.100.21 adc.example.local adc.example.local
bash-5.1# cat lookup.js
const dns = require('node:dns');
dns.lookup('adc.example.local', (err, address, family) =>
console.log('address: %j family: IPv%s', address, family));
bash-5.1# node lookup.js
address: "192.168.100.21" family: IPv4
Aşağıda bu işlem yapılırken aldığım network trafiği paketlerini görebilirsiniz.
Burada doğru cevap almamızın sebebi aslında tamamen rastlantı, internal olarak kullandığımız DNS sunucusu daha hızlı cevap verdiği için ilk ondan aldığı cevabı kabul edip bize doğru sonucu göstermiş.
Musl dökümanında mantığın nasıl çalıştığından bahsetmiş ama bunu aşağıdaki gibi diyagram olarak çizersek daha rahat anlaşılır.
sequenceDiagram
Client->>+Musl: What is the IP address of "adc.example.com"
par
Musl->>8.8.8.8: adc.example.com
and
Musl->>8.8.4.4: adc.example.com
and
Musl->>192.168.100.20: adc.example.com
end
192.168.100.20-->>Musl: 192.168.100.21
Musl-->>-Client: 192.168.100.21
8.8.8.8-->>Musl: No such name
8.8.4.4-->>Musl: No such name
Peki internal DNS sunucumuz geç cevap verse nasıl olacaktı? Hemen kendi ortamımızda bu gecikmeyi canlandıralım. Aşağıda tc
yani traffic control komutları ile
internal DNS sunucumuz olan 192.168.100.20’ye giden paketlere 5000ms gecikme ekliyoruz, bu şekilde bize geç cevap dönmüş olacak.
tc qdisc add dev eth0 root handle 1: prio priomap 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
tc qdisc add dev eth0 parent 1:2 handle 20: netem delay 0ms
tc filter add dev eth0 parent 1:0 protocol ip u32 match ip src `hostname -i` flowid 1:2
tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 5001ms
tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip dst 192.168.100.20 flowid 1:1
Ardından daha önce yaptığımız gibi komut satırından ve kod ile yazdığımız şekilde tekrar aynı ismi çözmeye çalışalım.
bash-5.1# getent hosts adc.example.local
bash-5.1# cat lookup.js
const dns = require('node:dns');
dns.lookup('adc.example.local', (err, address, family) =>
console.log('address: %j family: IPv%s', address, family));
bash-5.1# node lookup.js
address: undefined family: IPvundefined
Paket trafiğinden görüldüğü gibi gecikme işe yaramış, aslında bize iç sunucudan cevap gelmiş ama geç geldiği için, Musl ilk cevabı doğru kabul ederek yoluna devam etmiş ve bize, bu isim için herhangi bir adres çözemediğini belirtmiş.
Bu sefer son senaryo diyagram üzerinde aşağıdaki gibi işlemiş.
sequenceDiagram
Client->>+Musl: What is the IP address of "adc.example.com"
par
Musl->>8.8.8.8: adc.example.com
and
Musl->>8.8.4.4: adc.example.com
and
Musl->>192.168.100.20: adc.example.com
end
8.8.8.8-->>Musl: No such name
Musl-->>-Client: No such name
8.8.4.4-->>Musl: No such name
192.168.100.20-->>Musl: 192.168.100.21
Benim gibi Musl
kardeş neler yapıyor merak edenlere, fonksiyon çağrılarını izlemek için call trace işlemi başlatalım dedim.
Eğer debug sembolleri yoksa sadece dışarıya açılmış fonksiyon isimlerini vereceğinden , önce musl
için debug paketini ekledim. Böylece neler olup bitiyor daha rahat
görebiliriz.
bash-5.1# apk add musl-dbg
bash-5.1# apk add perf
bash-5.1# perf record --call-graph dwarf getent hosts adc.example.local
bash-5.1# perf report
62.50% 0.00% getent getent [.] hosts
|
---hosts
gethostbyname2
gethostbyname2_r
__lookup_name
name_from_dns_search (inlined)
|
|--50.00%--name_from_dns
| __res_msend_rc
| |
| |--25.00%--sendto
| | __alt_socketcall (inlined)
| | __syscall_cp_c
| | __cp_end
| | entry_SYSCALL_64_after_hwframe
| | do_syscall_64
| | __x64_sys_sendto
| | __sys_sendto
| | sock_sendmsg
| | inet_sendmsg
| | udp_sendmsg
| | udp_send_skb
| | ip_send_skb
Call stack içinde de görüldüğü gibi tüm hikaye gethostbyname2 içerisinden başlıyor ve devam ediyor. Kodu incelemek isteyenler linke tıklayabilir.
Görüldüğü gibi işletim sisteminin kullandığı C kütüphanesi ve onun çalışma mantığı DNS konusu olunca yazdığımız uygulamadan, işletim sisteminde bulunan araçlara kadar farklı şeyleri etkileyebiliyor. Hele Docker, Kubernetes gibi platformlar için isim çözümleme oldukça kritik, gitmek istediğiniz servislere önce çözümleme yaparak işe başlıyorlar. Musl ile Glibc işleyiş ve özellikler açısından farklılık gösterebiliyor, bir sonraki yazıda belki DNS ile alakalı diğer başımı ağrıtan Musl problemine değinirim ya da Glibc bu işe nasıl yapıyor ona bakabiliriz.
]]>Tcpdump
kaç defa hayatımı kurtardı sayısını hatırlamıyorum. Daha çok klasik
port, host ya da protokol üzerinden trafik kontrolü için kullanıyorum ama geçenlerde
karşılaştığım bir sorun sebebiyle daha ileri seviye filtreleme kullanmak zorunda kaldım.
Biraz da faydalı olacağını düşündüğüm ve egzersiz olsun diye izlediğim adımları paylaşmak istedim.
Canlı ortamda bizim sistemimizden ortamındaki sunucuların bazılarına ulaşamadığını
gördüm, sunuculara ulaşmamız için DNS
sunucusundan isim ile çözümleme yapması gerekiyor.
Bu yüzden DNS
sunucusuna hostname
bilgisinden IPv4
çözmek için atılan A
tipi sorguları analiz etmem gerekti.
Tshark
ya da Wireshark
gibi araçlarla aynı iş daha kolay yapılabilir ama unutmayın canlı ortamda çalışan bir
sistem var ve elimizde olan minimum araçla, yeni talep, yükleme değişiklik yapmadan
problemi tespit etmemiz lazım. Bu sunucuda yüklü olan bu işe en uygun araç tcpdump
ile
işe koyulalım.
DNS bildiğiniz gibi UDP protokolü üzerinden çalışıyor ve varsayılan olarak 53 portunu kullanıyor. Öncelikle ilgilendiğim network arayüzü üzerinden geçen DNS trafiğine bakmak için aşağıdaki gibi bir filtre kullandım.
tcpdump -ln -i ens160 'udp port 53'
10:10:28.000080 IP 192.168.100.169.62567 > 192.168.100.20.53: 21950+ SRV? _ldap._tcp.Default-First-Site-Name._sites.server.local. (73)
10:10:28.008770 IP 192.168.100.30.52098 > 192.168.100.20.53: 23304+ AAAA? hooks.slack.com.server.local. (47)
10:10:28.008770 IP 192.168.100.30.38794 > 192.168.100.20.53: 3665+ A? hooks.slack.com.server.local. (47)
10:10:28.158962 IP 192.168.100.32.43673 > 192.168.100.20.53: 9534+ A? arbiter1. (26)
10:10:28.230193 IP 192.168.100.22.61610 > 192.168.100.20.53: 24725+ A? au.download.windowsupdate.com. (47)
10:10:28.230454 IP 192.168.100.22.61610 > 192.168.100.21.53: 24725+ A? au.download.windowsupdate.com. (47)
10:10:28.702316 IP 192.168.100.21.57615 > 192.168.100.20.53: 40774+ SOA? server.local. (31)
10:10:28.954151 IP 192.168.100.133.58060 > 192.168.100.20.53: 37885+ AAAA? hooks.slack.com. (33)
10:10:28.954392 IP 192.168.100.133.54569 > 192.168.100.20.53: 19301+ A? hooks.slack.com. (33)
10:10:29.153919 IP 192.168.100.21.56348 > 192.168.100.20.53: 38375+ A? download.windowsupdate.com. (44)
10:10:29.415216 IP 192.168.100.21.54360 > 13.107.236.205.53: 19731 [1au] A? download.windowsupdate.com. (55)
10:10:29.454522 IP 13.107.236.205.53 > 192.168.100.21.54360: 19731*- 1/0/1 CNAME wu-fg-shim.trafficmanager.net. (98)
10:10:29.455627 IP 192.168.100.21.53126 > 192.168.100.20.53: 55492+ A? wu-fg-shim.trafficmanager.NET. (47)
10:10:29.600610 IP 192.168.100.33.53418 > 192.168.100.20.53: 52069+ A? arbiter1. (26)
10:10:29.703074 IP 192.168.100.21.57615 > 192.168.100.20.53: 40774+ SOA? server.local. (31)
Bu filtre sonucunda 53 portu üzerinden geçen tüm trafiği yukarıdaki gibi görebiliyoruz fakat
görüldüğü gibi bulmak istediğimiz kayıtlar dışında SRV,AAAA,CNAME..
gibi tüm DNS mesajlarını görüyoruz.
Fakat bizim istediğimiz sadece hangi adresler için Type A DNS Query
sorgularının yapıldığını tespit etmek.
Bunu yapmak için daha ileri seviye bir filtre yazmak dolayısıyla protokol detaylarına bakmamız gerekir. İlk olarak bilmemiz gereken DNS UDP üzerinde çalışan bir protokol ve bunu dikkate alarak bahsettiğimiz tipte DNS sorgularını nasıl bulabiliriz bulmaya çalışalım. DNS protokol başlık yapısı aşağıda görülebilir.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
-----------------------------------------------------------------
| Source Port | Destination Port |
-----------------------------------------------------------------
| Length | Checksum |
-----------------------------------------------------------------
| |
| Data |
| |
-----------------------------------------------------------------
Data
kısmının üstündeki satırlar protokol başlığını gösteriyor. Başlığı incelediğimizde 8 byte
uzunluğunda olduğunu ve içerdiği alanları görebiliyoruz.
DNS
paketleri ise Data
kısmının içerisinde geliyor, bu yüzden DNS
paket yapısına da bakmamız gerekiyor.
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
---------------------------------
| ID |
---------------------------------
| Flags |
---------------------------------
| QDCOUNT |
---------------------------------
| ANCOUNT |
---------------------------------
| NSCOUNT |
---------------------------------
| ARCOUNT |
---------------------------------
DNS paketinin yapısına baktığımızda her biri 2 byte
yer tutan 6 alan görüyoruz. Bu paket UDP paketi içinde Data
kısmında gönderilmiş olacak.
Bizim bulmamız gereken iki kısım var, birincisi Flags
alanı içeriğinden sadece Query
tipinde olan paketleri bulmamız lazım.
Daha iyi gözümüzde canlansın diye, DNS trafiği bulunan bir pcap
dosyasını Wireshark
üzerinde inceleyelim.
Yukarıdaki resim bizim bulmak istediğimiz Type A DNS Query
ve resme baktığımızda ID
alanının hemen altında olan Flags
içinde
hangi bit
değerinin 1
olarak gönderildiğini görebiliyoruz. Yanda da Flags
alanının gönderilmiş değerinin 0x0100
olduğu görülüyor.
O zaman şöyle yapabiliriz, UDP paket başlığı 8 byte
yer kaplıyor demiştik, üstüne 2 byte
DNS paketindeki ID
alanı geldi, hemen sonrasında
gelen Flags
alanı aslında udp[10]
içinde bulunması gerekiyor (dizi 0 ile başlıyor). Buradan şöyle bir filtre yazarak ilgili paketleri bulabiliriz
diye düşünüyorum.
tcpdump -ln -i ens160 'udp port 53 and udp[10] & 0x80 = 0'
tcpdump -ln -i ens160 'udp port 53 and udp[10] = 0x1'
tcpdump -ln -i ens160 'udp port 53 and udp[10] = 1'
Yukarıdaki filtrelerin hepsi aynı kapıya çıkıyor. & 0x80 = 0
nereden geldi diye sorulabilir, hemen açıklayalım. Tcpdump ile istersek bit flag
karşılaştırması yaparak
için aslında dolaylı yoldan oradaki değerin 0x0100
olduğunu kontrol edebiliriz. Yukarıdaki filtrelerden herhangi birini denediğimizde sonuç aşağıdaki gibi oluyor
08:50:00.092381 IP 192.168.100.61.4045 > 208.91.112.52.53: 43+ A? strict.bing.com. (33)
08:50:00.280419 IP 192.168.100.111.52874 > 192.168.100.20.53: 5935+ SRV? _ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.server.local. (83)
08:50:04.723430 IP 192.168.100.33.55386 > 192.168.100.20.53: 64594+ A? arbiter1. (26)
08:50:04.913121 IP 192.168.100.33.58165 > 192.168.100.20.53: 5767+ [1au] A? mongo03. (36)
08:50:04.913177 IP 192.168.100.33.53484 > 192.168.100.20.53: 54515+ [1au] AAAA? mongo03. (36)
08:50:04.914712 IP 192.168.100.33.44411 > 192.168.100.20.53: 44196+ [1au] A? mongo03. (36)
08:50:04.916585 IP 192.168.100.33.50264 > 192.168.100.20.53: 59818+ [1au] AAAA? arbiter1. (37)
08:50:04.916739 IP 192.168.100.33.36527 > 192.168.100.20.53: 52768+ [1au] A? arbiter1. (37)
08:50:04.918118 IP 192.168.100.33.36505 > 192.168.100.20.53: 1722+ [1au] A? arbiter1. (37)
08:50:04.918399 IP 192.168.100.33.59246 > 192.168.100.20.53: 19402+ [1au] AAAA? arbiter1. (37)
08:50:04.919751 IP 192.168.100.33.40935 > 192.168.100.20.53: 8112+ [1au] A? arbiter1.server.local. (51)
08:50:05.027957 IP 192.168.100.32.36793 > 192.168.100.20.53: 39115+ A? arbiter1. (26)
08:50:05.042618 IP 192.168.100.61.4045 > 208.91.112.52.53: 49402+ A? swscan.apple.com. (34)
Yukarıdaki paketlere baktığımızda ilk aşamada karşımıza çıkan DNS sorgu cevaplarının gittiğini görüyoruz ama hala listede
AAAA, SRV
gibi DNS sorgu istekleri görülüyor. Bunlardan kurtulmak için filtreyi son defa revize etmeye çalışalım.
DNS paket başlığının yapısını incelediğimizde, ilgili sorgu tipinin paketinin en sondan iki önceki 2 byte
içinde tutulduğunu görebiliyoruz.
Aşağıdaki resimden daha iyi anlaşılabilir.
Yani yapmak bulmak istediğimiz filtreleme kabaca şu şekilde olması gerekiyor.
udp[paketuzunlugu-4:2] = 0x0001
[a:b]
notasyonu a numaralı bytedan başlayarakb
kadar byte al demek.
Yukarıdan hatırlayacak olursanız UDP paket uzunluğu kendi başlığı içerisinde 2 byte
olarak 4:2
arasında tutuluyor.
Bu bilgiyi kullanarak tekrar filtreyi tekrar revize edelim.
tcpdump -ln -i ens160 'udp port 53 and udp[10] & 0x80 = 0 and udp[(udp[4:2]-4):2] = 0x0001'
09:37:26.521212 IP 192.168.100.32.50375 > 192.168.100.20.53: 57051+ A? arbiter1. (26)
09:37:26.522193 IP 192.168.100.32.35151 > 192.168.100.20.53: 56503+ A? arbiter1.server.local. (40)
09:37:26.523585 IP 192.168.100.32.34443 > 192.168.100.20.53: 53150+ A? arbiter1. (26)
09:37:26.524553 IP 192.168.100.32.40833 > 192.168.100.20.53: 53150+ A? arbiter1. (26)
09:37:26.525578 IP 192.168.100.32.46435 > 192.168.100.20.53: 51829+ A? arbiter1.server.local. (40)
09:37:26.727766 IP 192.168.100.33.60701 > 192.168.100.20.53: 60216+ A? arbiter1. (26)
09:37:26.729244 IP 192.168.100.33.48042 > 192.168.100.20.53: 60216+ A? arbiter1. (26)
09:37:26.730305 IP 192.168.100.33.35631 > 192.168.100.20.53: 58112+ A? arbiter1.server.local. (40)
09:37:27.087333 IP 192.168.100.32.48370 > 192.168.100.20.53: 25327+ A? arbiter1. (26)
09:37:27.088618 IP 192.168.100.32.37493 > 192.168.100.20.53: 25327+ A? arbiter1. (26)
09:37:27.089651 IP 192.168.100.32.50450 > 192.168.100.20.53: 21119+ A? arbiter1.server.local. (40)
Biraz zahmetli oldu ama görüldüğü gibi işe yaradı ve sadece A tipi DNS sorgularını filtreledik.
Yukarıda kullandığımız yöntem ilgili paketleri direk tcpdump
filtreleriyle bulmaktı. Bunun yerine tabi
aşağıdaki gibi klasik unix
araçlarından awk
kullanarak basit bir işlemle de işimizi tamamlayabilirdik.
tcpdump -ln -i ens160 "udp port 53" | awk '/A\?/{adr = $(NF-1); if(!d[adr]) { print adr; d[adr]=1; fflush(stdout) } }'
Aslında yaptığımız tüm sonuçları tcpdump
üzerinden alıp ilgilendiklerimizi awk
ile filtrelemek. Basit senaryolar
için bu tarz bir yöntem kullanılabilir fakat tcpdump
filtreleri arka planda BPF kullandığı ve
burada kernel
seviyesinde bir filtreleme olduğu için performans konusunda oldukça farklılık olacaktır.
Hangi adres için kaç tane DNS sorgusu atılmış canlı olarak güncellenen şekilde görmek için aşağıdaki awk
scriptini hazırladım.
tcpdump -ln -i ens160 'udp port 53 and udp[10] & 0x80 = 0 and udp[(udp[4:2]-4):2] = 0x0001' | awk '
{
adr=$(NF-1);
dict[adr]++;
system("clear")
system("tput cup 0 0")
for (key in dict)
print dict[key] " : " key
}
'
6 : swscan.apple.com.
12 : mongo03.
1 : play.google.com.
3 : autoupdate.opera.com.
1 : strict.bing.com.
208 : arbiter1.server.local.
1 : remote-host.server.local.
482 : arbiter1.
2 : remote-host.
1 : settings-win.data.microsoft.com.
1 : wpad.server.local.
Çalıştırdığımızda aşağıdakine benzer sonuçları görebilirsiniz. Gerisi hayal gücünüze kalmış
Terminal bazlı dosya yöneticilerinden bir süredir
nnn kullanıyordum. Oldukça hızlı olmasına
rağmen klavye kısa yollarını hatırlamakta zorlandığımı fark ettim. Farklı bir
araç araştırırken vifm ile karşılaştım. Adından
da anlaşılacağı gibi Vim
mantığı ile geliştirilmiş bir dosya yöneticisi.Bende
uzun süredir Vim kullanıcısı olduğu için genel kullanım mantığını anlamak
zaten kısa sürdü.
Ama gerçekten sevmeye geçenlerde hünerlerini görmeye başladığımda başladım
diyebilirim. Evdeki NAS sunucusunda arşiv dosyalarında düzenleme belirli bir
isim standardı uygulamak istedim. Bütün dosyalar öncelikle küçük harf olsun,
sonra örnek boşluk karakteri yerine -
olsun gibi. Bunun için bir bash
script yazılabilir tabi ama pratiklik açısından Vifm
yöntemi çok hoşuma
gitti.
Videoda görüldüğü gibi öncelikle ismini değiştirmek istediğimiz tüm dosyaları
aynı Vim
‘deki gibi CTRL+V G
ile seçiyoruz. Ardından yine Vim
benzeri cw
ile değiştirme işlemini başlatıyoruz. Sonrasında bize standart Vim
penceresi
getiriyor. Gelen ekranda ggguG
çalıştırınca tüm dosya isimleri küçük harfe
geçiyor ve son olarak :wq
ile kaydedip çıkıyoruz ve tüm dosya isimleri
değişmiş oluyor.
Ürünümüz kullanıcı ağ ortamına kurulup konfigürasyonları yapıldıktan sonra hedef sistemlere gerekli protokoller üzerinden erişim gerçekleştirip işlemlerini tamamlıyor. Fakat POC ortamında aşağıdaki gibi bir durumla karşılaştık.
Gerekli güvenlik duvarı kurallarının çoğu düzgün ayarlanmış fakat bir sunucu için gerekli kural unutulmuş. Bu sebeple ilgili sunucuya 22 portu üzerinden gitmeye çalıştığımızda erişim engeline takıldık ve POC ilerleyemedi. Ama resimde gösterdiğim gibi test için bize yardımcı olan müşterimiz bu iki ortama da gerekli portlardan erişebiliyor.
Tam süreç tıkandı derken aklıma SSH Reverse Tunnel kullanımı ile sorunu geçici de olsa çözebileceğimiz aklıma geldi. SSH Reverse Tunnel kısaca uzak bilgisayarın erişemediği fakat SSH tünelin açıldığı ortamın eriştiği bir kaynağa bir tünel oluşturarak ikisinin bu tünel içinden güvenli şekilde haberleşmesini sağlıyor. Yani bu yöntemi kullanarak ilgili kaynağa erişimi tünel üzerinden aşağıdaki gibi sağlayacağız.
Gelin bunu benzer bir senaryoda uygulayalım ve laboratuvar ortamımızda bulunan uygulama sunucusunu evimde bulunan NAS sunucusu ile haberleştirelim. Normalde ikisi tamamen birbirinden habersiz erişim mümkün değil. Bunun için aşağıdaki komutla SSH ters tünel oluşturuyorum. Ben MacOS üzerinde çalıştığım için OpenSSH kullanıyorum fakat bunu Windows üzerinde Putty ile de yapabilirsiniz, sadece komut yerine arayüzden ilgili ayarları yapmanız gerekecek.
[mypc] ssh -N -R localhost:22234:172.16.33.234:22 root@192.168.100.30 -vvv
Ev sunucusu 172.16.33.234 IP adresine sahip, laboratuvar ortamındaki sunucu 192.168.100.30 adresine sahip. Laboratuvar ortamındaki
sunucunun, kendi üzerinden localhost:22234
adresi ile evimdeki sunucunun 22
portuna bağlanmasını istiyorum.
-N
parametresini sadece tünel oluştursun bunu normal SSH erişimi için kullanmayacağımı belirtmek için kullanıyorum.
-R
parametresi ters tünel için kullanılıyor, -vvv
ise mümkün olduğunca bana fazla bilgi versin diye verbose
seviyesini arttırmak için
kullanılıyor.
Görüldüğü gibi tünel başarıyla oluşturuldu fakat bu aşamada henüz benim bilgisayarım üzerinden evdeki sunuya bağlantı yapılmış değil onu bu şekilde görebiliyorum. Aşağıdaki komutu kendi bilgisayarımda çalıştırdığımda bana boş çıktı veriyor
[mypc] lsof -i @172.16.33.234:22
Uzak sunucuda dinlenilen portları kontrol ettiğimde ise aşağıdaki gibi sshd
işleminin verdiğimiz portu dinlemeye başladığını görebiliyoruz
[remoteserver] ss -tulpn | grep 22234
tcp LISTEN 0 128 127.0.0.1:22234 *:* users:(("sshd",pid=25562,fd=9))
tcp LISTEN 0 128 [::1]:22234 [::]:* users:(("sshd",pid=25562,fd=8))
Şimdi aynı uzak sunucu üzerinden evdeki sunucuma bağlanmak için aşağıdaki gibi ssh bağlantısı kuruyorum.
[remoteserver] ssh username@localhost:22234
Ardından şifre girdiğimde evdeki sunucuma bağlantı yapabildim. Dikkat edin evdeki kişisel bilgisayarım üzerinden bu bağlantıyı yapmamama rağmen evdeki sunucu üzerindeki bağlantıları aşağıdaki gibi kontrol ettiğimde bağlantımın kişisel bilgisayarım üzerinden açılan tünel üzerinden yapıldığını görebiliyorum.
[nasserver] netstat -ant | grep 172.16.33.89
tcp 0 36 172.16.33.234:22 172.16.33.89:59815 ESTABLISHED
]]>Geçenlerde ortamlarımızdan birinde MongoDB container’ın aniden durduğunu farkettik. Önce tabi logları kontrol ettik fakat herhangi bir hata ya da uyarı logu gözükmüyordu. Problem tüm servisleri durdurup yeniden başlattığımızda birkaç gün çalışıp tekrar ediyordu. MongoDB üzerinde CPU/Memory/Loglar ve diğer değerler normal gözükmesine rağmen tüm servisler çalışırkerken MongoDB duruyordu.
Docker loglarından birşey çıkmayınca Linux host ortamını incelemeye başladık. CPU/Memory değerlerini geçmişe dönük incelerken Monitoring aracımızda tam MongoDB’nin kapanmasına yakın zaman aralığında available/used memory değerlerinin birleştiğini ve sonrasında MongoDB’nin kapandığını farkettik. Sorun aşağı yukarı belli olmuştu sorun sistemdeki mevcut belleğin tükenmesi nedeniyle kapanmasıydı fakat kapanmadan önce bile MongoDB Memory değerleri normal gözüküyordu.
MongoDB bellek değerleri normal gözüktüğüne göre başka bir sebeple durduruluyordu. Bunun için yine monitoring sistemine dönüp diğer servislerin ve host üzerindeki işlemlerin tükettiği değerleri kontrol ettik. O sırada herhangi bir işlem fazla bellek tüketmiyordu fakat diğer bir docker container adı “large_memory” olsun, belleğin büyük çoğunluğunu tüketip MongoDB’nin kapanmasına sebep oluyordu.
Bütün belleği tüketen container ayakta kalırken neden sadece MongoDB kapanıyordu ve bunu kim tetikliyordu? Bunu tespit etmek için Linux host üzerinde kernel loglarını incelemeye başladık. Dmesg ile logları incelerken ilgili zaman aralığına gittik ve aradığımız logu bulduk.
[2156113.520338] Out of memory: Kill process 24363 (mongod) score 52 or sacrifice child
[2156113.520726] Killed process 24363 (mongod), UID 999, total-vm:3079196kB, anon-rss:1292664kB, file-rss:0kB, shmem-rss:0kB
Linux out of memory management işlemcisi sistemi sürekli izleyip bellek yetersizliği durumunda hesapladığı skora göre kendine bir kurban seçip bu işlemi sonlandırıyor. Kurbanın nasıl seçildiğine dair detayları bağlantıdan ya da birçok farklı kaynaktan inceleyebilirsiniz fakat çok düz mantık en fazla kim bellek tüketiyorsa onu sonlandıralım mantığı ile çalışmıyor. Sistemde eğer bellek yetersiz kaldıysa, o anda oom_score değerine göre en yüksek hangi işlem ise onu sonlandırıyor. Bu sebepten dolayı malesef veritabanımız MongoDB işlemi mongod sonlanıyor, yani diğer container en fazla belleği tüketse bile, en fazla skora sahip olan mongod işlemi olduğu için o sonlanıyor.
Aynı senaryoyu test ortamında oluşturmak için kolları sıvadım. İlk olarak MongoDB’yi container olarak ayağa kaldırıp sisteminde boşta olan bellek miktarını kontrol ettim. Aşağıdaki gibi bir resim ortaya çıktı
watch free -h
Ardından docker container içinde çalışan tüm işlemlerin oom_score değerlerini bulacak oom.sh aşağıdaki scripti hazırladım.
#!/bin/bash
printf "%-50s %8s %8s\n" "Image" "PID" "OOM_SCORE"
docker ps --format " " |
while read -r line; do
id=$(echo "$line" | cut -d ' ' -f1)
img=$(echo "$line" | cut -d ' ' -f2)
topResult=$(docker top "$id" -o pid | grep -v "PID")
for r in $topResult;
do
score=$(cat /proc/"$r"/oom_score)
printf "%-50s %8s %8s\n" "$img" "$r" "$score"
done
done | sort -k3 -n -r
Linux bir işlemin oom skorunu /proc/pid/oom_score
altında tutuyor.
Yukarıdaki script çalıştığında o anda bulunan tüm docker containerlar içinde çalışan
tüm işlemleri azalan sıralı şekilde tüm container imajlarının adlarını ve yanlarında
OOM skor değerlerini yazacak.
Sıra geldi yük oluşturmaya yani bitene kadar bellek tüketmeye. large_memory container benzeri bir bellek kullanmak için aşağıdaki gibi basit bir container oluşturdum.
[host]docker run --name large_memory -d -t bash
[host]docker exec -it large_memory bash
[bash]for i in {1..12}; do cat /dev/zero | head -c 1000m | tail & done
Yukarıda container’ı interaktif modda ayağa kaldırıp run sonrasında kapanmasın diye ilk başta -d -t parametreleri ile çalıştırıyoruz. Ardından container içine girip for döngüsü ile her biri 1000MB tüketecek background job oluşturuyoruz.
cat /dev/zero | head -c 1000m | tail &
ile bellek tüketecek iş parçacığı oluşturuyoruz. Tail komutu son satıra gelene kadar tüm verileri belleğinde tuttuğu için
cat /dev/zero
ile sürekli null gönderip head -c 1000m
bunu 1000MB ile sınırlıyorum.
1000MB tüketen 12 tane işi özellikle oluşturdum çünkü bellekte 12GB civarı bir yer var bunun hepsini tüketmek istiyorum. 1000MB olmasının sebebi ise, bu testi yaptığım anda mongod yaklaşık 1.3GB değer tüketiyordu, yük için oluşturduğum işlemler daha fazla bellek tüketirse OOM tarafından sonlandırılmasını istemiyorum.
Birkaç dakika sonra, oluşturduğumuz yeni container içindeki işler tüm belleği tüketiyor ve bellekte yer kalmıyor ve neredeyse MongoDB’nin 10 katı bellek tüketiyor.
Fakat o sırada oom.sh scriptini çalıştırdığımızda aşağıda gördüğünüz gibi mongod en tepede yani bellek kalmadığında bir süre sonra sonlandırılacak işlem olarak gözüküyor.
Hemen sonrasında aynı gerçek ortamda yaşadığımız gibi test ortamında da MongoDB kapanıyor ve bekletiğimiz gibi kernel mesajlarında aynı uyarıyı alıyoruz.
Senaryoyu test ortamında simule etmemizin mutluğunu yaşıyoruz. Gerçek ortamda yaşadığımız problem,MongoDB ile alakalı olmamasına rağmen diğer bir container içinde çok fazla küçük küçük process oluşturulup bunların toplamda fazla bellek tüketmesi ve Linux OOM yönetimi tarafından sonlandırılmaya en uygun aday olarak mongod seçilip sonlandırılmasıydı.
Bu sorunu kullandığımız bir kütüphane sebebiyle yaşadığımızı tespit ettik ve sorunu biraz farklı bir şekilde process’leri kendimiz öldürerek aştık.
Bazen container içinde oluşan işlemler tamamen izole gibi düşünülse de, böyle bir durum yok. Aynı kernel üzerinde çalışıp aynı belleği paylaşıyorlar, dolayısıyla host işletim sistemi ile aradaki ilişkiyi iyi bilmek gerekiyor. Fırsat bulursam host ile docker arasındaki ilişkiyi farklı açılardan değerlendirip buraya da yazmak istiyorum.
]]>