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"