Fehlerhafte SD-Karte auslesen
Stand der Dinge: 13.10.2018, 04.08.2017
Ab und zu bekomme ich mal eine fehlerhafte SD- oder MicroSD-Karte in die Hand, mit der Bitte, doch mal zu versuchen noch was zu retten. Meistens handelt es sich dabei um eine defekte FAT, weshalb ich mit TestDisk oder PhotoRec innerhalb kurzer Zeit schon alles gerettet habe, was dem Besitzer wichtig ist. In ganz selten Fällen wird die Karte vom System (Linux oder Windows) gar nicht mal erkannt, so als ob gar keine Karte eingelegt wurde. Da muss ich bis jetzt auch das Handtuch werfen, aber ich arbeite schon an einer Mikrocontroller-Lösung, um dieses Problem eventuell auch in den Griff zu bekommen.
Hier soll es jetzt aber um ein Zwischending gehen. Im konkreten fall habe ich eine fast volle 16GB MicroSD-Karte aus einem Smartphone bekommen, von der ich möglichst viele Bilder retten sollte. Mit PhotoRec konnte ich auf die ersten ca. 1GB zugreifen, danach ging es nicht mehr weiter. In den wiederhergestellten Bildern befand sich eine kleine (11kB) JPG-Datei, die allerdings mehrere hundert mal wiederhergestellt wurde, obwohl sie nur ein mal auf der Karte vorhanden war. Ich erkläre mir das dadurch, dass die SD-Karte, nachdem sie auf einen fehlerhaften Cluster gestoßen ist, immer wieder den letzten noch guten Cluster zurückgegeben hat, egal auf welcher Adresse.
Zum Beispiel so: Cluster 1 bis 100 sind in Ordnung, Cluster 101 ist fehlerhaft. Wenn ich jetzt der Reihe nach einlese, bekomme ich bei Cluster 101 einen Fehler und bei allen weiteren Clustern 102, 103, 104, ... immer die Daten von Cluster 100 zurück. Auch wenn ich jetzt nochmal den guten Cluster 1 anfrage, bekomme ich stattdessen die Daten von Cluster 100 zurück. So weit, so schlecht.
Als möglicher Lösungsweg kam mir dann folgendes in den Sinn. Wenn das Auslesen immer nur bis zu einem fehlerhaften Cluster (oder Block *) funktioniert, müsste ich nach einem jeden Solchen die Karte rausnehmen, wieder einlegen, und dann beim nächsten Cluster (oder Block) weiter auslesen.
* SD-Karten sind üblicherweise in Blöcken zu 512 Byte organisiert. Das ist also die kleinste Einheit, die auf einmal gelesen oder geschrieben werden kann. Bei allen mir bisher begegneten Karten und Dateisystemen werden diese Blöcke nochmal zu sogenannten Clustern mit 4kB zusammengefasst. Ein Cluster umfasst also 8 Blöcke. Bei meiner fehlerhaften Karte habe ich festgestellt, dass quasi ausnahmslos [hab' sie nicht alle überprüft, aber ziemlich viele] alle Lesefehler exakt auf einer Clustergrenze aufgetreten sind, und auch alle 7 nachfolgenden Blöcke fehlerhaft waren.
Eine Software, die das so machen kann, habe ich leider nicht gefunden. Mag sein das es sie gibt, aber ich habe mir dann mal wieder was eigenes einfallen lassen. Ob das unter Windows überhaupt möglich ist weiss ich nicht, deshalb habe ich mich gleich für Linux entschieden. Als Entwicklungsumgebung diente mir der Laptop meiner Frau [weil ich meinen derzeit verliehen hatte] und eine ''Ultimate Boot CD'', meine lieblings-Live-Distri. Hier die erste, noch manuelle Version:
#!/bin/bash # if [ "$#" -ne 3 ]; then echo "Usage: ddsd [input device] [output file] [start sector]" >&2 exit 1 fi inputdev=$1 outputdev=$2 startblock=$3 endblocks=$(blockdev --getsize $inputdev) endblock=$[$endblocks -1] copygood=0 copybad=0 echo "Sektoren: $endblock" read -p "ENTER zum Starten oder STRG+C zum abbrechen..." date +"%T" for ((i=$startblock;i<=$endblock;++i)); do echo -ne "Kopiere Sektor ${i} von ${endblock}...\r\c" dd if=$inputdev bs=512 ibs=512 skip=$i count=1 status=none >> $outputdev if [ "$?" != "0" ]; then echo "FEHLER bei Sektor $i" 1>&2 #512 Nullen >> $outputdev printf '\0%.0s' {1..512} >> $outputdev copybad=$[$copybad +1] i=$[$i +1] read -p "SD-Karte entfernen, 5s warten, wieder einstecken, ENTER druecken..." #exit 1 else copygood=$[$copygood +1] fi done echo -ne "\n" date +"%T" echo "Fehlerfrei kopierte Sektoren: $copygood" echo "Fehlerhafte Sektoren: $copybad"
Das Programm macht folgendes: Es guckt nach wieviele Cluster auf der Karte vorhanden sind und liest dann jeweils immer nur einen einzigen Block per dd ein. Wenn das klappt werden die Daten an eine Image-Datei angehängt. Wenn nicht, wird stattdessen ein leerer Cluster angehängt, damit die neue Image-Datei die richtige Länge behält. Außerdem pausiert das Programm und wartet darauf, dass der Anwender die SD-Karte (oder gleich den ganzen Kartenleser) entfernt und nach kurzer Wartezeit wieder anschließt. Die Wartezeiten von 3-4 Sekunden sind dabei ziemlich wichtig, weil es sonst passieren kann dass das System die Karte nicht wieder erkennt und somit weitere Fehler produziert. Dann kann man ENTER drücken und es wird weiter eingelesen, bis wieder ein Fehler auftritt oder die Karte komplett eingelesen wurde.
Mit so kleinen Blocks dauert das Ganze natürlich ewig. Deshalb habe ich das Script dann auf 4kB-Cluster erweitert. Das Ganze hat beim ersten Versuch schon recht gut funktioniert. Bei 16GB und 4kB Clustern ist die Datenrate natürlich ziemlich besch...eiden (bummelig 1MB pro Sekunde) und bei über 500 defekten Clustern (also ebensovielen Kartenleser-raus-und-rein-Vorgängen!) hat das Ganze 6 bis 7 Stunden gedauert. Die eine USB-Buchse ist dadurch auch recht stark beansprucht worden und mechanisch etwas ausgelutscht, aber die Datenrettung war soweit erfolgreich. Hinterher dann noch PhotoRec über das Image gejagt und 7000+ Fotos gerettet.
Allerdings fiel mir am Ende auf, dass das gerettete Image nicht exakt der Originalgröße entsprach. Die Ursache dafür stellte sich dann nach weiteren Versuchen raus: Ab und zu dauerte das Auslesen eines Clusters mehrere Sekunden, verursachte aber keinen Fehler. Dennoch wurden nicht alle 8 Blöcke korrekt ausgelesen, wodurch dann auch keine 4kB Daten ans Image angehängt wurden, sondern weniger. Also habe ich noch eine Abfrage eingebaut, die nach jedem Cluster das bisherige Image auf korrekte Länge überprüft und bei Bedarf mit Nullen auffüllt, sowie einen Fehler meldet.
Nach dem ganzen Zirkus habe ich denn darüber sinniert, dass das doch auch einfacher gehen müsste. Dafür bräuchte ich nur eine Möglichkeit, die Karte oder den Leser kurz stromlos zu machen und dann wieder anzuschließen. Einen Roboter zu bauen, der die Karte rauszieht und wieder einsteckt erschien mir doch reichlich weit hergeholt. Einen USB-Adapter, der per seriellem Kommando ein USB-Gerät kurz trennt und wieder verbindet erschien mir auch recht aufwändig. Glücklicherweise habe ich dann ein Linux-Hausmittel gefunden, um die Arbeit zu erledigen. Nach einigem Browsen fand sich eine Möglichkeit, ein einzelnes USB-Gerät per authorize zu resetten. Das klappte auch zuerst ganz gut, aber nach etwa einem Dutzend Mal gab's dann immer wieder Hardware-Probleme, die sich nur mit einem Systemneustart wieder beheben ließen. Eine weitere Möglichkeit ist, das komplette USB-System zu deaktivieren. Dadurch geht dann natürlich nix mehr was per USB angebunden ist (Maus, Tastatur, Sticks oder Festplatten, etc.), aber der Kartenleser wird deutlich zuverlässiger neu gestartet. Leider war aber auch das keine perfekte Lösung. Nach einigen dutzend Deaktivierungen streikte wieder mal die Hardware und ließ sich nicht mehr aktivieren. Um sicherzugehen dass es nicht nur an dem einen Laptop liegt, habe ich dann noch auf einem weiteren Laptop und einem normalen PC getestet. Beide male war kein vollständiges Image ohne weitere Eingriffe zu bekommen.
Also musste doch eine Hardwarelösung her. Um es möglichst einfach und universell zu halten, kam mir die Idee den eingebauten Systemlautsprecher als GPIO zu missbrauchen. Ein mit 'beep' erzeugter Signalton müsste sich mit wenig Hardware-Aufwand abfragen lassen und zum Schalten eines Relais verwenden lassen. Und so war es auch. Die beiden Pins vom Lautsprecher liegen im Ruhezustand auf 5V, der Negative wird bei einem beep mit entsprechender Frequenz gegen Masse geschaltet. Zwei Transistoren, ein kleiner Elko und ein paar Widerstände reichen aus, um damit ein 5V-Relais zu schalten. Das Ganze wird praktischerweise gleich von einem USB-Port mit 5V versorgt und direkt in ein USB-Kabel eingeschleift, an dem dann der Kartenleser angeschlossen ist. Funktionierte einwandfrei. Allerdings nur am PC, nicht am Laptop. Da gibt es leider keinen Systemlautsprecher. Also habe ich nochmal versucht, das Ganze über die Soundkarte und den Line-Out zu realisieren. Geht eigentlich auch ganz gut, allerdings ist der Pegel am Line-Out deutlich kleiner als die 5V beim Systemlautsprecher. Bei meiner Schaltung mit einer BE-Strecke und einer Schottky-Diode (also ca. 1V Spannungsabfall) reichte die Lautstärke am PC mit Realtek-Soundchip auf maximaler Lautstärke gerade so aus um das Relais zu schalten. Einen Ticken leiser und es schaltete nichts mehr... Also vorher einmal den alsamixer starten und die Lautstärke raufsetzen.
#!/bin/bash # if [ "$#" -ne 3 ]; then echo "Usage: ddsd [input device] [output file] [start cluster]" >&2 exit 1 fi inputdev=$1 outputdev=$2 startblock=$3 modprobe pcspkr badlog="${outputdev}.log" clustermult=128 clustersize=$((clustermult * 512)) blocksize=$(blockdev --getsize $inputdev) cardsize=$((blocksize * 512)) endblocks=$(($blocksize / $clustermult)) endblock=$(($endblocks -1)) copygood=0 copybad=0 printf "Kartengroesse: %'.f Bytes\n" "$cardsize" printf "Clustergroesse: %'.f Bytes\n" "$clustersize" printf "Cluster: %'.f\n\n" "$endblocks" read -p "ENTER zum Starten oder STRG+C zum abbrechen..." date +"%T" >> $badlog printf "Datenmenge: %'.f Bytes\n" "$cardsize" >> $badlog printf "Cluster zu %'.f Bytes: %'.f\n" "$clustersize" "$endblocks" >> $badlog for ((i=$startblock;i<=$endblock;++i)); do prozent=$(echo "scale=6; $i/$endblock*100" | bc) printf "Kopiere Cluster %'.f (${prozent}%%)\r" "$i" dderr=$(dd if=$inputdev bs=$clustersize ibs=$clustersize skip=$i count=1 2>&1 >> $outputdev) if [ "$?" != "0" ]; then printf "FEHLER bei Cluster %'.f " "$i"; date +"%T" echo $i >> $badlog for ((k=1;k<=$clustermult;++k)); do printf '\0%.0s' {1..512} >> $outputdev done copybad=$[$copybad +1] echo -ne "warten auf Karte...\r\c" sleep 2 #Nach dem Fehler kurz warten... echo -ne "auswerfen... \r\c" #### Auswerfen per PC-Speaker: # beep -l 3000 #### Auswerfen per Soundkarte: speaker-test -t sine -f 1000 -l 1 > /dev/null echo -ne "wieder einhaengen...\r\c" sleep 5 #Kartenleser wieder aktivieren und wieder kurz warten... else sizeist=$(stat -c%s "$outputdev" 2> /dev/null) sizesoll=$(( ($i + 1) * $clustersize)) if [ "$sizeist" -lt "$sizesoll" ]; then printf "FEHLER: Cluster %'.f zu kurz " "$i"; date +"%T" dd if=/dev/null of=$outputdev bs=1 count=1 seek=$sizesoll 2> /dev/null echo $i >> $badlog copybad=$[$copybad +1] else copygood=$[$copygood +1] fi fi done date +"%T" >> $badlog printf "\n\n Fehlerfrei kopierte Cluster: %'.f von %'.f\n" "$copygood" "$endblocks" printf "\n\n Fehlerhafte Cluster: %'.f von %'.f\n" "$copybad" "$endblocks"
Wenn du dieses Script aus Windows raus kopierst und in eine Datei packst, denke dran später unter Linux einmal dos2unix drüberlaufen zu lassen, sonst gibt's sehr seltsame Fehler beim Ausführen!
Meine aktuelle Version des Skripts ist ein bisschen 'gründlicher' geworden. Man kann jetzt einstellen dass ein fehlerhafter Cluster erst ein paar mal erneut eingelesen werden soll, und erst wenn das nicht klappt der Kartenleser getrennt wird. Wenn es auch nach mehrmaligem Auswerfen nicht mehr weiter geht, bricht das Skript ab. Dann sollte man erstmal den Rechner neu starten und es ab diesem Cluster erneut versuchen. Wenn das auch nichts mehr bringt, geht's halt beim nächsten Cluster weiter.
Den beep für den PC-Lautsprecher habe ich hier inzwischen rausgelassen, weil es doch immer blöd ist jedes mal den Rechner auseinander zu basteln. Die Variante mit dem Line Out klappt besser, und PC's ohne Soundkarte gibt es heutzutage eh keine mehr.
#!/bin/bash # # ddsd V2.3 # preamp.org 2018 if [ "$#" -ne 3 ]; then echo "Teste Auswurf..." >&2 speaker-test -t sine -f 1000 -l 2 > /dev/null echo "Usage: ddsd [input device] [output file] [start cluster]" >&2 exit 1 fi inputdev=$1 outputdev=$2 startblock=$3 retries=2 #Cluster erneut einlesen thoroughretries=1 #Laufwerk auswerfen, dann erneut einlesen badlog="${outputdev}.log" clustermult=128 #128 * 512 = 64kB clustersize=$((clustermult * 512)) blocksize=$(blockdev --getsize $inputdev) cardsize=$((blocksize * 512)) endblocks=$(($blocksize / $clustermult)) endblock=$(($endblocks -1)) copygood=0 copybad=0 printf "Kartengroesse: %'.f Bytes\n" "$cardsize" printf "Clustergroesse: %'.f Bytes\n" "$clustersize" printf "Cluster: %'.f\n" "$endblocks" printf "Versuche: %'.f einfach, %'.f gruendlich\n\n" "$retries" "$thoroughretries" read -p "ENTER zum Starten oder STRG+C zum abbrechen..." date +"%T" >> $badlog printf "Datenmenge: %'.f Bytes\n" "$cardsize" >> $badlog printf "Cluster zu %'.f Bytes: %'.f\n" "$clustersize" "$endblocks" >> $badlog printf "\nStarte Kopiervorgang um "; date +"%T" #printf "\n" read_cluster() { dderr=$(dd if=$inputdev bs=$clustersize ibs=$clustersize skip=$1 count=1 2>&1 >> $outputdev) if [ "$?" != "0" ]; then for ((x=1;x<=$clustermult;++x)); do printf '\0%.0s' {1..512} >> $outputdev done return 1 else sizeist=$(stat -c%s "$outputdev" 2> /dev/null) sizesoll=$(( ($1 + 1) * $clustersize)) if [ "$sizeist" -lt "$sizesoll" ]; then #Groesse passt nicht dd if=/dev/null of=$outputdev bs=1 count=1 seek=$sizesoll 2> /dev/null #mit Nullen auffuellen return 2 else copygood=$[$copygood +1] fi fi } karte_auswerfen() { sleep 2 #Nach dem Fehler kurz warten... speaker-test -t sine -f 1000 -l 2 > /dev/null sleep 20 #Kartenleser wieder aktivieren und wieder kurz warten... testblocksize=$(blockdev --getsize $inputdev) #Device testen if [ "$blocksize" != "$testblocksize" ]; then echo $i >> $badlog date +"%T" >> $badlog printf "$(date +"%T") Abgebrochen bei Cluster %'.f! Karte nicht mehr lesbar!\n" "$i" exit fi } for ((i=$startblock;i<=$endblock;++i)); do prozent=$(echo "scale=6; $i/$endblock*100" | bc) printf "$(date +"%T") Kopiere Cluster %'.f von %'.f (${prozent}%%), %'.f fehlerhaft (%'.f)\r" "$i" "$endblocks" "$copybad" "$lastbad" for ((k=0;k<=$retries;++k)); do read_cluster $i case "$?" in 0) #alles ok, keine wiederholungen continue 2 ;; *) #1=Lesefehler, 2=Cluster unvollstaendig continue ;; esac done for ((k=0;k<=$thoroughretries;++k)); do karte_auswerfen read_cluster $i case "$?" in 0) #alles ok, keine wiederholungen continue 2 ;; *) #1=Lesefehler, 2=Cluster unvollstaendig continue ;; esac done echo $i >> $badlog lastbad=$i copybad=$[$copybad +1] done #date +"%T" date +"%T" >> $badlog printf "\n\nKopiervorgang abgeschlossen um "; date +"%T" printf "\nFehlerfrei kopierte Cluster: %'.f von %'.f\n" "$copygood" "$endblocks" printf "Fehlerhafte Cluster: %'.f von %'.f\n\n" "$copybad" "$endblocks"