Top 100 instrukcji asemblera x86-64

Gynvael Coldwind, Dan Murray

2024-08-12

W trakcie przygotowywania warsztatów wprowadzających do inżynierii wstecznej i asemblera, zadaliśmy sobie oczywiste pytanie: od których z setek instrukcji asemblera x86-64 powinno się zacząć?

Zwyczajowo instrukcji assemblera uczy się tematycznie – najpierw podstawowe kopiowanie danych, operacje arytmetyczne i bitowe. Następnie skoki bezwarunkowe, a po nich testy i porównania sparowane ze skokami warunkowymi. Potem przychodzą zazwyczaj instrukcje operujące na stosie, a zaraz za nimi CALL/RET. A po tym "oczywista" do tej pory kolejność zaczyna się nieco rozmywać. Ale czy to faktycznie jest właściwa kolejność? Czy naprawdę ważniejszym jest dowiedzenie się co robi instrukcja JS od wszędobylskiego CALL, szczególnie jeśli ostatecznym celem nauki asemblera jest inżynieria wsteczna?

Te pytania powinny być rozważone z różnych stron, ale jedna rzecz jaka może pomóc w znalezieniu odpowiedzi to zawsze dane!

Biorąc pod uwagę powyższe, Dan oskryptował Ghidrę w Pythonie i policzył ile razy dana instrukcja pojawiła się w plikach wykonywalnych – w tym w bibliotekach dynamicznych – znalezionych pod Windowsem oraz Ubuntu (Linux). Dla doprecyzowania dodamy, że pisząc instrukcja mamy konkretnie na myśli unikatowe mnemoniki, takie jak generuje je disassembler Ghidry.

Wyniki prezentują się następująco:

#InstrukcjaOdsetek
1.MOV35.14%
2.CALL7.97%
3.LEA6.83%

Zobacz całe top 100 pod tekstem posta...

Bazując na tych danych można przyjąć, że jeśli nauczysz się dziesięciu najpopularniejszych instrukcji asemblera z tej listy, to będziesz w stanie zrozumieć 75% kodu programu. Ucząc się kolejnych dziesięciu instrukcji dochodzimy do 90% pokrycia. Oczywiście potem pozostaje nam bardzo długi ogon innych instrukcji – zaobserwowaliśmy ich ponad 800, a wiemy, że jest ich więcej. Trzeba też zaznaczyć, że rozumienie instrukcji, które składają się na kod programu, to jeszcze nie to samo co rozumienie algorytmu, ale jest to zdecydowanie dobry pierwszy krok.

Kilka informacji o tej tabeli:

  • Rozważanie popularności instrukcji jest interesujące i może się przysłużyć podczas projektowania szkoleń, ale inne zastosowania mogą wymagać nieco bardziej dokładnego podejścia (np. podczas projektowania procesorów – przy próbach ustalenia gdzie poświęcić więcej czasu na optymalizację).
  • Kolumna "Odsetek" mówi o tym ile razy daną instrukcje zaobserwowaliśmy w całym badanym zbiorze. Procent ten jest obarczony błędem, o czym zresztą piszemy dalej na tej liście. Co za tym idzie, lepiej nie sugerować się za bardzo konkretną wartością procentową. Bardziej istotne jest to, czy instrukcja jest czy nie jest często spotykana, a mniej istotne to czy powinna być na np. 55. czy 57. miejscu.
  • Zgodnie z tym, należy pamiętać, że to jest lista najczęściej występujących unikatowych instrukcji, zaobserwowanych w kodzie (w tzw. deadlistingach). W szczególności nie jest to lista najczęściej wykonywanych instrukcji.
  • Wszelkie instrukcje, które pojawiają się dynamicznie w pamięci, np. jako rezultat rozpakowywania kodu albo kompilacji JIT, również nie zostały wzięte pod uwagę przy tworzeniu tej listy.
  • Ponieważ źródłem instrukcji był output Ghidry, pewne idiosynkrazje specyficzne dla Ghidry również mają tutaj zastosowanie. Na przykład, prefiks LOCK w Ghidrze pojawia się jako sufiks w nazwie instrukcji. W związku z tym, np. INC i INC.LOCK pojawiają się jako osobne wpisy w tabeli, odpowiednio na pozycji 29. i 78. Jeśli by założyć, że jest to jednak ta sama instrukcja, wtedy znalazłaby się na miejscu 28.
  • Trzeba też zwrócić uwagę na inne różnice w wynikach działania różnych dezasemblerów. Przykładowo, niektóre dezasemblery używają sufiksów oznaczających wielkość danych na jakich instrukcja operuje (np. MOVL albo MOV.L). Inne mogą używać reprezentacji NOPn (np. NOP3) do oznaczenia "nic nie robiących" wielobajtowych instrukcji w miejscu faktycznych instrukcji wynikających z kodowania.
  • Biorąc pod uwagę problem z rozróżnieniem co jest a co nie jest kodem w programie (zwykle rozwiązywany za pomocą dużej ilości heurystyki), zapewne wdarł się tutaj niewielki błąd również na tym froncie. Podobnie w przypadku błędów, gdy przebieg dezasemblera został rozpoczęty na złym offsecie (tj. wewnątrz faktycznie wygenerowanej instrukcji) – na szczęście to ma tendencje się bardzo szybko synchronizować.
  • Są pewne różnice w kolejności i instrukcjach, które pojawiają się w top 100 pomiędzy Windowsem a Ubuntu. Wynika to z tego, że na różnych platformach używane są różne kompilatory, które różnie generują kod wynikowy.
  • Podobnie, rezultaty będą inne np. na Gentoo, zwłaszcza jeśli używa się niestandardowych ustawień kompilatora w swoim make.conf.
  • I na koniec, lista popularności instrukcji będzie się również różnić pomiędzy kodem trybu użytkownika a kodem jądra. Stwierdzając to co oczywiste, uprzywilejowane instrukcje nie pojawiają się w programach w trybie użytkownika, poza ewentualnymi błędami i oczywiście lokalnymi exploitami.

Top 100 najpopularniejszych instrukcji x86-64

#InstrukcjaOdsetek
1.MOV35.14%
2.CALL7.97%
3.LEA6.83%
4.CMP4.98%
5.JZ4.15%
6.TEST4.04%
7.POP4.03%
8.JMP4.00%
9.PUSH3.98%
10.ADD3.07%
11.JNZ2.79%
12.XOR2.51%
13.SUB1.76%
14.RET1.15%
15.MOVZX1.02%
16.AND0.91%
17.NOP0.82%
18.MOVUPS0.74%
19.ENDBR640.67%
20.MOVAPS0.61%
21.JA0.52%
22.INT30.45%
23.SHR0.41%
24.MOVSXD0.41%
25.OR0.39%
26.SHL0.36%
27.JC0.31%
28.JBE0.28%
29.INC0.26%
30.JNC0.25%
31.MOVDQA0.23%
32.XORPS0.22%
33.ROR0.20%
34.SAR0.19%
35.IMUL0.19%
36.JS0.19%
37.JNS0.19%
38.JLE0.17%
39.MOVDQU0.14%
40.JG0.13%
41.MOVSD0.11%
42.MOVQ0.11%
43.CMOVZ0.10%
44.MOVSS0.10%
45.SETZ0.09%
46.DEC0.09%
47.UD10.09%
48.BT0.09%
49.MOVSX0.07%
50.SETNZ0.07%
51.CMOVNZ0.07%
52.PXOR0.06%
53.JL0.06%
54.JGE0.06%
55.XADD.LOCK0.06%
56.NEG0.06%
57.DEC.LOCK0.06%
58.CMOVS0.05%
59.CMOVNC0.05%
60.SUB.LOCK0.05%
61.PADDD0.05%
62.NOT0.04%
63.ROL0.04%
64.MOVD0.04%
65.PUNPCKLQDQ0.04%
66.CDQE0.04%
67.CMOVC0.04%
68.SBB0.04%
69.CMOVNS0.04%
70.CMPXCHG.LOCK0.03%
71.VMOVDQA0.03%
72.ADC0.03%
73.UCOMISS0.02%
74.MOVAPD0.02%
75.DIV0.02%
76.CMOVA0.02%
77.PSHUFD0.02%
78.INC.LOCK0.02%
79.PADDW0.02%
80.CMOVBE0.02%
81.BSWAP0.02%
82.MULSS0.02%
83.CMOVO0.02%
84.MUL0.02%
85.LFENCE0.02%
86.SETC0.02%
87.VPADDD0.02%
88.CMOVL0.02%
89.CVTSI2SD0.02%
90.CVTSI2SS0.02%
91.ADDSS0.02%
92.UCOMISD0.02%
93.FSTP0.02%
94.PAND0.02%
95.UD20.02%
96.FLDZ0.02%
97.MULSD0.02%
98.POR0.02%
99.CMOVGE0.01%
100.PMADDWD0.01%

Nie przegap nic od HexArcana! Dodaj nasz blog do swojego czytnika RSS/Atom lub zapisz się na nasz newsletter poniżej.