Felsökning med GDB: Digging Deeper

0
155
Shutterstock/Nicescene

Den kraftfulla GNU Debugger GDB återvänder till första steget. Vi dyker djupare in i stackar, backtraces, variabler, core dumps, ramar och felsökning än någonsin tidigare. Gå med på en helt ny, mer avancerad introduktion till GDB.

Vad är GDB ?

Om du är ny i felsökning i allmänhet eller GDB & # 8212; GNU Debugger & # 8212; i synnerhet kanske du vill läsa vår felsökning med GDB: Komma igång-artikeln och sedan återgå till den här. Den här artikeln fortsätter att bygga vidare på den information som presenteras där.

Installera GDB

Installera GDB på din Debian/Apt-baserade Linux-distribution (som Ubuntu och Mint), kör följande kommando i din terminal:

sudo apt install gdb

För att installera GDB på din RedHat/Yum-baserade Linux-distribution (som RHEL, Centos och Fedora), kör följande kommando i din terminal:

sudo yum install gdb

Travar, backtraces och ramar!

Det låter som äpplen, pajen och pannkakorna! (Och i viss utsträckning är det det.) Precis som äpplen och pannkakor matar oss, är staplar, backspår och ramar bröd och smör för alla utvecklare som debuggar i GDB, och informationen som presenteras i dem matar riktigt en utvecklare som är hungrig att upptäcka sin eller hennes fel i källkoden.

RELATED Felsökning med GDB: Komma igång

Kommandot bt GDB genererar en backtrace av alla funktioner som anropades, en efter en, och presenterar oss för ramarna (funktionerna) listade, en efter en. En stack är ganska lik en backtrace genom att en stack är en översikt eller lista över funktioner som ledde till en krasch, situation eller problem, medan en backtrace är det kommando vi ger för att få en stack.

Med detta sagt används ofta termerna omväxlande, och man kan säga & # 8220; Kan du få mig en stack? & # 8221; eller & # 8220; Låt oss se backtrace, & # 8221; vilket något vänder på betydelsen av båda orden i varje mening.

Och som en uppfriskning från vår tidigare artikel om GDB, en ram är i grunden en enda funktion listad i en backtrace av alla kapslade funktionsanrop & # 8212; till exempel, huvudfunktionen () börjar först (listas i slutet av en backtrace) och sedan main () kallad math_function (), som i sin tur kallas do_the_maths () etc.

Om detta låter lite komplicerat, ta en titt på Felsökning med GDB: Komma igång först.

För entrådiga program kommer GDB, lika bra som alltid (om inte alltid), att upptäcka den kraschande (och enda) tråden korrekt när vi startar vårt felsökningsäventyr. Detta gör det enkelt att omedelbart köra kommandot bt när vi går in i gdb och befinner oss vid (gdb) -prompten, eftersom GDB omedelbart visar oss den bakspårning som är relevant för kraschen som vi observerade.

Engängad eller flertrådad?

En mycket viktig sak att observera (och veta) vid felsökning av kärndumpar är om programmet som felsöker är (eller kanske mer specifikt, < i> var ) engängad eller flertrådad?

I vårt tidigare exempel/artikel tittade vi på en enkel backtrace, med en uppsättning ramar som visas från ett självskrivet program. Programmet var engängat: Inga andra körningstrådar gafflades inifrån koden.

Men så snart vi har flera trådar kommer ett enda bt-kommando (backtrace) producera bara backtrace för den tråd som för närvarande är vald i GDB.

GDB väljer automatiskt kraschkraschen, och även för flertrådade program är detta 99% + av tiden korrekt gjort. Det finns bara enstaka tillfällen där GDB misstag den kraschande tråden för en annan. Till exempel kan detta hända om programmet har kraschat i två trådar samtidigt. Under de senaste tio åren har jag bara observerat detta mindre än en handfull gånger när jag hanterade tusentals kärndumpar.

För att visa skillnaden mellan exemplet som användes i vår senaste artikel och en riktig multitrådad applikation, byggde jag MySQL-server 8.0.25 i felsökningsläge (med andra ord, med felsymboler/instrumentering tillagda) med hjälp av build-skriptet i MariaDB-a GitHub repo och körde SQL-data om pquery-ramverket mot den för lite, vilket snart nog kraschade MySQL-felsökningsservern.

Som du kanske kommer ihåg från vår tidigare artikel är en kärndump en fil som produceras av operativsystemet, eller i vissa fall av själva applikationen (om den har inbyggda krashanterings-/kärndumpningsbestämmelser), som sedan kan analyseras med hjälp av GDB. En kärnfil skrivs vanligtvis som en begränsad behörighetsfil (för att skydda konfidentiell information som finns i minnet), och du kommer sannolikt att behöva använda ditt superanvändarkonto (dvs. root) för att komma åt den.

Låt & # 8217 ; s dyk direkt in i kärndumpen som produceras med gdb bin/mysqld $ (ls data/* core *):

Och några sekunder senare slutför GDB laddningen och tar oss till GDB-prompten:

De olika Nya LWP meddelanden (som var ännu fler i hela produktionen) ger en bra antydan om att detta program var multitrådat. Uttrycket LWP står för Light Weight Process . Du kan tänka dig att det motsvarar en enda tråd vardera, tillsammans gör du en lista över alla trådar som GDB upptäckte när du analyserade kärnan. Observera att GDB måste göra detta på förhand så att det kan hitta den kraschande tråden som beskrivs tidigare.

Som vi kan läsa på den sista raden i den första GDB-startbilden ovan initierade GDB också en lässymbol från bin/mysqld-åtgärden. Utan felsökningssymbolerna inbyggda/kompilerade i binären hade vi sett några eller de flesta ramar markerade med ett funktionsnamn ??. Dessutom skulle inga variabla avläsningar presenteras för dessa funktionsnamn.

Det här problemet (oupplösliga ramar ses vid felsökning av specifika optimerade/avskalade binärer som har avlägsnat/avlägsnat sin felsymbol) är inte lätt att lösa. Om du till exempel skulle se detta på en binär databasserver på produktionsnivå (där felsökningssymbolerna är avlägsnade/borttagna för att optimera körning osv.) Måste du följa generellt mer komplexa procedurer, till exempel hur man Skapa en Full Stack-spårning för mysqld.

Backtraces!

Eftersom vi har sammanställt MySQL-servern med felsökningssymboler ingår en backtrace kommer att visa alla funktionsnamn korrekt i vårt fall. Vi utfärdar ett bt-kommando vid (gdb) -prompten, och vår backtrace-utdata är som följer:

Så, hur ser vi en backtrace för alla trådar eller en annan tråd? Detta kan uppnås genom att använda kommandotråden tillämpa alla bt eller tråd 2; bt, respektive. Vi kan byta ut 2 i det sista kommandot för att komma åt en annan tråd osv. Medan tråden applicerar är all bt-utdata lite detaljerad att infoga här, här är utdata när du byter till en annan tråd och får en backtrace för den tråden:

Om du läser igenom alla datorfelloggar eller -spår kommer det, precis som alltid, att avslöja fler detaljer lätt att missa när man bara tittar på information. Det här är en riktig skicklighet. En av mina tidigare IT-chefer gjorde mig medveten om det stora behovet av att göra det, och jag överlämnar härmed samma information till alla ivriga läsare av denna artikel. För att stödja detta uttalande med några bevis, ta en närmare titt på den producerade backtrace, och du kommer att märka termerna listen_for_connection_event , poll , Mysqld_socket_listener, and connection_event_loop for Mysqld_socket_listener . Det är helt klart: Den här tråden väntar på inmatning.

Detta är bara en ledig tråd som troligen väntade på att en MySQL-klient skulle ansluta eller mata in ett nytt kommando eller något liknande. Med andra ord skulle det vara lika bra som nollvärde att fortsätta felsöka den här tråden.

Detta leder oss också tillbaka till hur praktiskt det är att GDB automatiskt presenterar den kraschande tråden för oss vid start. Allt vi behöver göra för att starta vårt felsökningsäventyr är att få en backtrace. Sedan, när man analyserar flera trådar och deras interaktion, är det vettigt att hoppa mellan trådarna med trådkommandot. Observera att detta kan förkortas till t:

Intressant här har vi tråd 3, som också finns i någon avfrågningsslinga och ser ut som (LinuxAIOHandler :: enkät), även om det i det här fallet är på OS/disknivå (som betecknas av termerna Linux , AIO och Handler) och en närmare titt avslöjar att det väntar, det verkar som om AIO ska slutföra: fil_aio_wait.

Som du kan se finns det mycket information om tillståndet för ett program när det kraschar, vilket kan ses från loggarna om man tittar noga nog.

Här är ett tips: Du kan använda kommandot set log in i GDB om du vill spara all information på disken så att du enkelt kan söka efter utdata senare och du kan använda set log off för att avsluta utgångsspår. Informationen lagras som standard på gdb.txt.

Hoppa in i ramar

Precis som vi såg är det möjligt att växla mellan trådar och till och med få en backtrace för alla stockar på en gång, och det är lika möjligt att hoppa in i enskilda ramar! Vi kan till och med & # 8212; förutsatt att källkoden finns på skivan och är lagrad på den ursprungliga skivplatsen (dvs. samma källkodskatalog som användes när produkten byggdes) & # 8212; se källkoden för en särskild ram som vi befinner oss i.

Här måste man vara försiktig. Det är ganska enkelt att inte matcha binärfiler, kod och kärndumpar. Att försöka analysera en kärndump som skapats med version v1.0 för ett visst program kommer förmodligen inte att vara kompatibel med den version v1.01 binär som sammanställts något senare med v1.01-koden. Man kunde inte [alltid] använda v1.01-källkoden för att felsöka en kärndump skriven med version v1.0 i ett program, även om v1.0-binären också är tillgänglig.

< p> Ordet alltid placerades som ett valfritt, som ibland & # 8212; om koden i det avsnittet i koden och programmet som felsöks inte har ändrats sedan den senaste versionen & # 8212; det kan vara möjligt att använda äldre källkod.

Denna praxis är kanske misslyckad, eftersom några enkla ändringar i koden kan göra att kodraderna inte längre motsvarar den binära och/eller kärndumpen. Det är bäst att antingen aldrig blanda olika versioner av källkoden, binärfilerna och kärndumparna, eller att bara förlita sig på kärndumpen och den binära, båda av samma version, utan källkoden eller med endast källkoden hänvisas till manuellt .

Fortfarande, om du analyserar många kärnor som kunder skickade in, ofta med begränsad information, ibland kan man komma undan med att använda en något annorlunda version av källkoden, och kanske till och med en något annan binär (mindre troligt) , även om du alltid inser att den information som presenteras av GDB mycket sannolikt kommer att vara ogiltig delvis, eller mer sannolikt, helt. GDB kommer också att varna dig vid start om det kan upptäcka en oöverensstämmelse mellan kärnan och binären.

För vårt exempel är källan, binären och kärndumpen gjorda med samma version av källkoden och med varandra, och vi kan därför med glädje lita på GDB när den producerar utdata som backtraces.

Det finns ett annat litet undantag här, och det är stackkrossning. I ett sådant fall kommer du antingen att observera felmeddelanden i GDB, se ?? ramnamn som liknar situationen som beskrivs ovan (men den här gången, på grund av oläsbarheten hos en kärndump i kombination med den binära) & # 8212; eller stacken kommer att se riktigt udda och felaktiga ut. För det mesta kommer det att vara helt klart. Ibland kan ett riktigt dåligt fel orsaka stackkrossning.

Låt oss nu hoppa in i en ram och se hur några av våra variabler och kod ser ut:

Variabler

< img src = "http://www.cloudsavvyit.com/pagespeed_static/1.JiBnMqyl6S.gif" />

t 1 bt f 7 ram 8 p * thd p thd

Här angav vi olika kommandon för att navigera till höger tråd och köra en backtrace (t 1 tog oss till den första tråden, den kraschande tråden i vårt exempel, följt av backtrace-kommandot bt) och hoppade därefter till ram 7 och sedan ram 8 med kommandona f7 respektive ram 8. Du kan se hur man, liknande trådkommandot, kan förkorta ramkommandot till dess första bokstav, f.

Slutligen försökte vi komma åt thd-variabeln, även om detta optimerades ur spåret/core dump för just denna ram. Informationen är dock tillgänglig om vi helt enkelt hoppar in i rätt ram, som har variabeln tillgänglig och inte optimerades (lite försök och fel kan krävas):

Under de senaste två skärmdumparna ovan visade jag två olika sätt att skriva utskriften (igen, förkortat på samma sätt som bara p), det första med en ledande * för variabelnamnet, den andra utan).

Den intressanta biten här är att den andra används oftare, men i allmänhet bara ger en minnesadress för variabeln i fråga, vilket inte är särskilt användbart. Kommandots * version (p * thd) löser variabeln till dess fullständiga innehåll istället. GDB känner också till variabeltypen, så det finns ingen anledning att skriva ut (kasta värdet till en annan variabel typ).

Wrapping up

I den här mer ingående GDB-guiden tittade vi på stackar, backtraces, variabler, core dumps, ramar och felsökning. Vi studerade några GBD-exempel och gav några viktiga tips för den ivriga läsaren om hur man felsöker bra och framgångsrikt. Om du gillade att läsa den här artikeln, ta en titt på artikeln Hur Linux-signaler fungerar: SIGINT, SIGTERM och SIGKILL.