Ich versuche den Sinn dieses ARM codeschnippsel zu verstehen, mit der
Bitte um Kommentierung. Der Code stammt aus einer Steuergeräte-Firmware.
Im Einsprung ist R4 mit einer Speicheradresse im RAM geladen. Hier
können 2-Byte Werte von 0xF100 an aufwärts stehen.
1) R5 wird mit 0 geladen
2) R0 wird mit dem Byte aus Speicheradresse R4+1 geladen
3) R1 wird mit dem Byte aus Speicheradresse R4+2 geladen
4) R0 wird um 8 Bit nach links geschoben (die rechts eintretenden Bits
sind 0)
5) R0 wird mit R0 + R1 geladen
6) R0 wird um 16 Stellen nach links geschoben
7) R0 wird um 16 Stellen nach rechts geschoben
8) R3 wird mit 0xF101 geladen
9) R0 wird mit R3 verglichen (Flags werden gesetzt)
10) Bei Gleichheit von R0 und R3 wird nach loc_36a2 verzweigt, ansonsten
geht es nach diesem Befehl weiter.
Aus meiner Sicht sind die Befehle 2-5 dazu da einen 16 Bit Wert mit dem
offset R4+1 in das Register R0 zu laden.
Die Befehle 6 und 7 sind quasi eine Maskierung der oberen 16 Bit auf 0
(hier wäre auch ein AND R0 mit 0x0000FFFF genauso gut gewesen, evtl. ist
Shift schneller?)
Olli Z. schrieb:> Die Befehle 6 und 7 sind quasi eine Maskierung der oberen 16 Bit auf 0
Richtig. Das ist allerdings unnötig, weil diese Bits durch das LDRB
bereits auf 0 gesetzt wurden. Das passiert bei schlechter/abgeschalteter
Compiler-Optimierung, besonders wenn man zwischen verschiedenen
Integertypen hin und her wandelt. IIRC machen alte GCC-Versionen sowas.
r0 und r1 einzeln zu laden (statt direkt 16bit mit LDRH zu laden) wird
wahrscheinlich gemacht weil die Adresse nicht aligned ist (d.h. R4 ist
gerade, dadurch ist R4+1 ungerade).
Olli Z. schrieb:> (hier wäre auch ein AND R0 mit 0x0000FFFF genauso gut gewesen, evtl. ist> Shift schneller?)
Den Immediate-Wert 0x0000FFFF zu laden ist auch aufwendig. Ist es ein
ARMv6 oder v7? Weiß die Details gerade nicht auswendig, aber ich meine
insbesondere bei ARMv6 dürfte das 2x shiften effizienter sein.
Ja, die Firmware stammt aus 2005 rum, vielleicht sogar noch älter. Danke
für die Erklärungen, dann liege ich ja nicht so falsch :-)
Das hier macht übrigens ein IDA Pro ARM-Decompiler daraus:
1
signed int __fastcall uds_readDataByIdentifier(unsigned __int8 *a1)
2
{
3
unsigned __int8 *v1; // r4
4
signed int v2; // r5
5
int v3; // r0
6
unsigned int v4; // r0
7
unsigned int v5; // r0
8
unsigned int v6; // r0
9
int v8; // [sp+0h] [bp-30h]
10
11
v1 = a1;
12
eeprom_i2c_init_enable();
13
v2 = 0;
14
v3 = ((v1[1] << 8) + v1[2]) & 0xFFFF;
15
...
16
}
Da wünscht man sich doch einen Decompiler mit ChatGPT Unterstützung der
einen richtig schönen C/C++ Code aus dem ARM generiert :-)
Olli Z. schrieb:> Ja, die Firmware stammt aus 2005 rum, vielleicht sogar noch älter. Danke> für die Erklärungen, dann liege ich ja nicht so falsch :-)> [code]> signed int __fastcall uds_readDataByIdentifier(unsigned __int8 *a1)
Oh, UDS und der RDBI, das mache ich immer noch beruflich. Ich schätze,
das war damals CAN-basiert. Heutzutage mit DoIP.
Thomas
Ein weiteres Artefakt was vermutlich seinen Ursprung in fehlender
Compiler-Optimierung hat?
Ich finde öfter solche Konstrukte im Code die auf den ersten Blick wenig
Sinn machen:
1
LDR R1, =byte_0x400017F0
2
MOVS R0, #8
3
STRB R0, [R1,#(byte_400017F3 - 0x400017F0)]
4
MOVS R0, #0x30
5
STRB R0, [R1,#(byte_400017F4 - 0x400017F0)]
6
MOVS R0, #0x61
7
STRB R0, [R1,#(byte_400017F5 - 0x400017F0)]
8
MOVS R0, #0x55
9
STRB R0, [R1,#(byte_400017F6 - 0x400017F0)]
10
MOVS R0, #0xAA
11
STRB R0, [R1,#(byte_400017F7 - 0x400017F0)]
Da werden 8-Bit Werte im Speicher (RAM) gesetzt, vermutlich ein Array.
In R1 wird die Basis-Adresse geladen und dann nachfolgend in den STRB
die Index-Methode genutzt. Der Offset wird dabei jedesmal berechnet aus
der Differenz zweier 32-Bit Speicheradressen. Ich hätte da jetzt eher
einfach einen Index gesehen, z.B.
1
LDR R1, =byte_0x400017F0
2
MOVS R0, #8
3
STRB R0, [R1,#3]
4
MOVS R0, #0x30
5
STRB R0, [R1,#4]
6
MOVS R0, #0x61
7
STRB R0, [R1,#5]
8
MOVS R0, #0x55
9
STRB R0, [R1,#6]
10
MOVS R0, #0xAA
11
STRB R0, [R1,#7]
Denke ich da einfach wieder nicht wie ein Compiler, oder gibt es auch
hierfür eine gute Erklärung?
Thomas S. schrieb:>> signed int __fastcall uds_readDataByIdentifier(unsigned __int8 *a1)> Oh, UDS und der RDBI, das mache ich immer noch beruflich. Ich schätze,> das war damals CAN-basiert. Heutzutage mit DoIP.
Ja genau, auch wenn das hier prinzipiell nichts zur Sache tut, aber ich
versuche gerade eine Automotive Firmware in Teilen zu verstehen
(Reverse-Engineering) und gleichzeitig ARM-Code lesen zu lernen. Es ist
sicher nicht möglich eine komplette Firmware zu rückwärts zu entwickeln,
aber bestimmte Komponenten und daraus wird man dann wieder schlauer was
die Funktion angeht.
Olli Z. schrieb:> Da werden 8-Bit Werte im Speicher (RAM) gesetzt, vermutlich ein Array.
Ja, einzelne Byte-Zugriffe wahrscheinlich wieder wegen Alignment.
Olli Z. schrieb:> Der Offset wird dabei jedesmal berechnet aus> der Differenz zweier 32-Bit Speicheradressen.
Das ist ein Artefakt des Disassemblers, ARM-Instruktionen können so
etwas gar nicht enthalten.
Olli Z. schrieb:> Ich hätte da jetzt eher> einfach einen Index gesehen, z.B.
Genau das steht da auch in den Instruktionen. Das "byte_400017F3 -
0x400017F0" hat der Disassembler da "hilfreich" eingefügt damit du
siehst dass es an die Stelle byte_400017F3 kommt.
PS:
Olli Z. schrieb:> Ja, die Firmware stammt aus 2005 rum, vielleicht sogar noch älter.
Also ist es vermutlich ein ARM7TDMI, der war ja sehr verbreitet. Dessen
Thumb Instruction Set kann maximal 11bit Immediates in eine Instruktion
kodieren, und auch das nur für Sprünge. Für arithmetische Operationen
und Speicherzugriffe sind nur 8bit-Immediates möglich:
https://developer.arm.com/documentation/ddi0210/c/Introduction/Instruction-set-summary/Thumb-instruction-summary?lang=en
Somit können eine Bitmaske wie 0x0000FFFF oder gar 2 32bit-Werte wie
"byte_400017F7 - 0x400017F0" bei Weitem nicht in einer Instruktion
kodiert werden.
Laut Datenblatt vom Prozessor wird hier ein 16-Bit Wert hochgezählt mit
Überlauf. D.H. diese Routine gibt einen "Zufallswert" im Bereich
0000-FFFF zurück, im Takt des Bit-Clocks für diesen Port (125 kbit/s).
Interessant ist die Verwendung davon:
1
000031B4 createRandom
2
000031B4 PUSH {R4-R6,LR}
3
000031B6 MOVS R4, #0
4
000031B8 LDR R5, =seed ; Hier hin werden die berechneten Zufallsdaten geschrieben
5
000031BA loc_31BA
6
000031BA BL mscan_timerValue ; Hier nach enthält R0 den 16-Bit Zufallswert
7
000031BE MOVS R1, #0
8
000031C0 loc_31C0
9
000031C0 LSLS R3, R0, #1 ; Multiplikation mit 2, also R3=R0*2
Soweit so gut (stimmt hoffentlich alles?)
Kommen wir zum Endgegner ;-)
1
00003214 LDRB R0, [R5,#2]
2
00003216 MOVS R1, R5
3
00003218 CMP R0, #0
4
0000321A BNE loc_3230
5
0000321C CMP R4, #5
6
0000321E BCC loc_322C
7
8
00003220 MOVS R0, #3
9
00003222 STRB R0, [R1]
10
00003224 MOVS R0, #0x1D
11
00003226 STRB R0, [R1,#1]
12
00003228 MOVS R0, #0x65
13
0000322A STRB R0, [R1,#2]
14
15
0000322C loc_322C
16
0000322C CMP R4, #5
17
0000322E BCC loc_31BA
18
19
00003230 loc_3230
20
00003230 POP {R4-R6}
21
00003232 POP {R3}
22
00003234 BX R3
1
if(seed[2]==0)return;
2
if(R4>5){
3
seed[0]=0x03;
4
seed[1]=0x1D;
5
seed[2]=0x65;
6
}
7
}while(R4<=5);
Nochmal zusammengefasst und etwas optimiert auf Lesbarkeit:
1
uint8n=0;
2
uint8seed[3];
3
uint16rnd;
4
do
5
{
6
rnd=timerValue();
7
for(uint8i=0;i<=5;i++)
8
{
9
rnd=(rnd+(rnd*2)+29)&0xFFFF;
10
}
11
seed[1]=(rnd&0xFF);
12
seed[2]=(rnd>>8);
13
14
rnd=timerValue();
15
for(uint8i=0;i<=5;i++)
16
{
17
rnd=((rnd*29)+3)&0xFFFF;
18
}
19
seed[0]=(rnd&0xFF);
20
seed[1]=seed[1]^(rnd>>8);
21
22
if(seed[0]==0||seed[1]==0||seed[2]==0)return;
23
24
n=n+1;
25
if(n>5)
26
{
27
seed[0]=0x03;
28
seed[1]=0x1D;
29
seed[2]=0x65;
30
}
31
}while(n<=5);
Wie man erkennt legt die Routine großen Wert darauf das im Ergebnis
keine 0x00 Bytes vorkommen.
Eines macht mich stutzig... die "if (n>5)" würde ja praktisch bei der
letzten Iteration aufgerufen und alle so mühsam zuvor berechneten Werte
mit diesen statischen überschreiben?!?
Ich glaube ich habe diesen code falsch interpretiert
1
000031BE MOVS R1, #0
2
000031C0 loc_31C0
3
...
4
000031CA ADDS R1, #1
5
000031D0 CMP R1, #5
6
000031D2 BCC loc_31C0
Das Carry-Flag wird bereits gesetzt wenn R1 den Wert 5 hat. Damit hab
ich nicht gerechnet, denn Carry ist für mich ein Übertrag und CMP macht
doch nichts anderes als SUBS R1, 5# wo ja 0 raus kommt. Das finde ich
reichlich merkwürdig, ja sogar unlogisch irgendwie...
So wäre die richtige Schleife also:
Bei manchen Prozessoren entspricht das C-Flag einer Subtraktion nicht
der intuitiven Vorstellung, sondern der Addition des Einerkomplements.
Das ist dann genau umgekehrt.
Für sporadische Tests habe ich diesen Online-ARM Assembler/Debugger
gefunden: https://cpulator.01xz.net/?sys=arm
Damit kann man flux mal Code (Assembler) einstellen und durchsteppen.