Введение
Недавно у меня появилась мысль научиться считать в шестнадцатеричной системе счисления. Так как я человек ленивый, такие способы, как выучить таблицу умножения меня не устраивали. Немного поразмыслив, я вспомнил, что каждое утро решаю (иногда несколько раз) несложный пример, чтобы отключить будильник. Помогает слабо, со временем я начал решать почти не просыпаясь. Так почему бы не совместить полезное с полезным?
Осталось выбрать способ реализации. Так как я не имею не малейшего представления о разработке под андроид (да и вообще с Java не сильно знаком), да и писать свое приложение ради такой мелочи — это стрельба из пушки по воробьям, было решено модифицировать уже имеющийся будильник.
![](https://habrastorage.org/storage1/7da8ab24/5f11a081/2db2147a/404a7dc9.png)
Под катом вы найдете описание инструментов, процесса и результата перевода примера в hex. А также объяснение синтаксиса smali кода (язык опкодов для виртуальной машины dalvik). Картинок почти нет, буков много.
Немного о структуре .apk
Приложение .apk — это файл упакованный zip'ом. Внутри содержатся файлы ресурсов, AndroidManifest.xml и classes.dex. Второе — это файл в котором содержатся основные сведения о приложении, как-то: информация об основном Activity, список прав, запрашиваемых при установке, и прочая не важная для наших целей информация. classes.dex — скомпилированный для виртуальной машины dalvik байт-код. Его мы можем перевести в jar и получить более или менее читаемый код на Java. Чем мы сейчас и займемся.
Получение читабельного кода
Итак, берем файл нашего приложения (в моем случае это AlarmDroid) и разархивируем его. Есть много способов сделать это, я пользовался Apk Manager, о котором я еще напишу чуть ниже. Далее нужно конвертировать classes.dex в jar. Для этого есть dex2jar. Перетаскиваем файл на dex2jar.bat. Теперь нужно научиться читать получившийся jar файл. Используем для этого программу jd-gui (кстати есть одноименный плагин для Eclipse).
Итак мы получили почти читаемый код, хотя кое-где декомпилятор явно плющит.
Декомпиляция и чтение .smali файлов
Так как java код read-only, исправлять придется в опкодах виртуальной машины. Их всего чуть меньше 256 штук. Для декомпиляции можно использовать упомянутый выше Apk Manager или более консольный apktool. Впрочем первое является лишь красивой оберткой второго, с добавлением некоторых дополнительных функций(например разархивация и выбор уровня сжатия). После декомпиляции получаем папку с файлами с расширением .smali. Это и есть те самые опкоды. Их уже можно читать любым текстовым редактором. Но неплохо было бы обзавестись какой-нибудь подсветкой. Я пользовался подсветкой для Notepad++, но есть так же и для vim.
Ну, с инструментами, наконец разобрались, пора и код анализировать.
Первая кровь
Для начала неплохо было бы сделать генерацию чисел не до 10, а до 15. Изучаем код, и находим метод генерации примера. Поискать его придется, классов в деревьях много. Но так как названия методов сохранились, это не очень сложно.
Находим искомый метод в классе RingerActivity.
![](https://habrastorage.org/storage1/b181a816/4e98393d/9d8a5545/2dd8d2e5.png)
Ну вроде все, просто: генерируем 3 числа и 2 знака, и кладем результат в this.x (это нам, кстати, пригодится).
Теперь находим этот же метод в smali-коде (к сожалению в онлайне нету хайлайтеров smali кода, а свой писать лениво, поэтому код будет скринами).
![](https://habrastorage.org/storage1/bc0fd673/e07d6ead/592a392a/cf88be4d.png)
(Код метода продолжается дальше)
Код непонятный, но явно видно что константы задаются один раз в начале. Поэтому меняем 0xa на 0xf и пытаемся увидеть изменения, чтобы знать что мы на верном пути. С помощью Apk Manager собираем, подписываем, перекачиваем на телефон (можно тестировать на эмуляторе, если установлен Android SDK), устанавливаем, ставим будильник на минуту вперед, ждем, смотрим. Эту процедуру придется повторять большое количество раз. Числа на экране увеличились, ошибок не возникает. Отлично! Теперь самое время немного разобраться в непонятном smali коде.
Разбираемся в smali коде
В общем-то все довольно хорошо описано вот тут, я лишь вкратце и по-русски перескажу.
Типы, методы, поля
В dalvik опкодах есть два вида типов: примитивные(primitive) и ссылочные(reference). Примитивные типы обозначаются одной буквой (например I — int, V — void), полный список здесь.
Ссылочные типы — это объекты и массивы, или, иными словами, все кроме примитивных типов. В коде обозначаются как
Lpackage/name/ObjectName;где L в начале означает, что это объект, а; — конец имени. Данная запись будет эквивалентна
package.name.ObjectNameв Java.
Массивы имеют вид [I — это одномерный массив интов(в Java будет int[]). Для большего количества измерений просто добавляем квадратных скобочек в начале. [[[I == int[][][] (максимальное количество измерений — 255). Разумеется можно создавать массивы ссылочных типов, массив строк, например, будет выглядеть вот так:
[Ljava/lang/String;
Методы задаются длинным образом, который включает в себя объект содержащий метод, имя метода, типы всех передаваемых параметров, и тип возвращаемого значения.
Lpackage/name/ObjectName;->MethodName(III)ZВ этом примере Lpackage/name/ObjectName; — это объект, содержащий метод, MethodName — имя метода, (III) — типы передаваемых параметров (в данном случае 3 инта), и Z — тип возвращаемого значения (в данном случае bool).
Вот еще один более сложный пример.
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
На Java этот код будет выглядеть так:
String method(int, int[][], int, String, Object[])
Поля классов задаются следующим образом:
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
Здесь в общем-то все и так ясно.
Регистры
В smali коде все регистры 32 битные и могут содержать любой тип значений. Для 64 битных значений используются 2 регистра.
Определение количества регистров в методе
Есть два способа определить количество регистров. Директива .registers определяет общее количество регистров, в то время как .locals определяет количество регистров, не являющихся параметрами метода.
Передача параметров
При вызове метода аргументы попадают в последние регистры. Т.е. если метод вызывается с 2 аргументами и 5 регистрами(v0-v4), аргументы попадают в регистры v3 и v4.
Первый параметр в нестатических методах — это всегда объект, от которого был вызван метод. Таким образом если вы вызываете нестатический метод
LMyObject;->callMe(II)V
в него, помимо двух интов, передается LMyObject; перед обоими интами, и получается, что в методе 3 параметра. И если вы задали общее количество регистров в методе 5 (или локальное количество 2), то в v2 попадет объект, в v3 — первый инт, в v4 — второй.
Именование регистров
Есть 2 схемы именования регистров: общая v-схема, и p-схема для регистров параметров. В вышеизложенном примере у нас будут 5 общих регистров v0-v4, и 3 регистра параметров p0-p2, причем как было сказано выше p0 и v2 (а также p1 и v3, p2 и v4) обозначают один и тот же регистр.
В общем есть еще некоторые тонкости, но нам для изменения будильника они не важны.
Перевод примера в hex
Теперь, когда со smali кодом стало все ясно, неплохо было бы перевести пример в hex. Свою функцию писать очень уж не хотелось, поэтому я обратился к гуглу. И нашел метод Integer.toHexString().
Итак находим нужный метод
private void updateProblemView()и его же в smali коде
{
TextView localTextView = this.problem;
String str1 = String.valueOf(this.a);
StringBuilder localStringBuilder1 = new StringBuilder(str1).append(" ");
int i = this.p;
String str2 = op2text(i);
StringBuilder localStringBuilder2 = localStringBuilder1.append(str2).append(" ");
int j = this.b;
StringBuilder localStringBuilder3 = localStringBuilder2.append(j).append(" ");
int k = this.q;
String str3 = op2text(k);
StringBuilder localStringBuilder4 = localStringBuilder3.append(str3).append(" ");
int m = this.c;
String str4 = m + " = ";
localTextView.setText(str4);
}
.method private updateProblemView()V
.locals 3
.prologue
.line 520
iget-object v0, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->problem:Landroid/widget/TextView;
new-instance v1, Ljava/lang/StringBuilder;
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->a:I
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
invoke-direct {v1, v2}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
const-string v2, " "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->p:I
invoke-direct {p0, v2}, Lcom/splunchy/android/alarmclock/RingerActivity;->op2text(I)Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->b:I
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->q:I
invoke-direct {p0, v2}, Lcom/splunchy/android/alarmclock/RingerActivity;->op2text(I)Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->c:I
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v1
const-string v2, " = "
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
.line 521
return-void
.end method
Для первого числа в примере все просто. Заменяем
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;на
invoke-static {v2}, Ljava/lang/Integer;->toHexString(I)Ljava/lang/String;и дело с концом. С остальными двумя чуть посложнее, они не переводясь в String добавляются в результат. Поэтому заменяем
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->b:Iна
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v1
iget v2, p0, Lcom/splunchy/android/alarmclock/RingerActivity;->b:I(обратите внимание, что тип передаваемого значения изменился). Аналогично делаем с третьим числом.
invoke-static {v2}, Ljava/lang/Integer;->toHexString(I)Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v1
Запускаем — видим маленькие буковки. А хочется то большие! Не проблема, дописываем перед добавлением в строку
invoke-virtual {v2}, Ljava/lang/String;->toUpperCase()Ljava/lang/String;для всех трех чисел.
move-result-object v2
Изменение поля ввода
В общем-то на этом можно было остановится, считать стало на порядок сложнее. Но сейчас тренируется перевод из hex в dec, а считать мы продолжаем в десятичной. Это не то чего хотелось. Поэтому необходимо преобразовать поле ввода.
Первым делом, чтобы закончить с методом update добавим «0x» перед полем ввода. Ну это совсем просто, изменяем
const-string v2, " = "на
const-string v2, " = 0x"Далее нужно изменить тип поля ввода с числового на текстовый (числовой hex, к сожалению, не предусматривает). Подобные свойства хранятся в xml файлах. Тут мне помог поиск по файлам по ключевым словам «arithmetic», «math» и подобным. В файле res/layout/ringer.xml находим строчку
гуглим, находим список значений inputType, меняем на «text».<EditText android:textSize="36.0dip" android:gravity="left" android:id="@id/math_solution" android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@null" android:inputType="phone" />
Теперь осталось научится преобразовывать ввод так, чтобы он совпадал с результатом вычисленным при генерации (RingerActivity.x, помните?). Ищем и находим следующий код
…Понять что-либо довольно сложно, но видим некоторую связь solution и com/splunchy/android/alarmclock/RingerActivity$9. Оказывается последнее — это smali файл, который, судя по всему jd-gui не смог адекватно отобразить. Идем туда и находим нужный нам метод сравнения!
label1621: label1912: for (boolean bool15 = false; ; bool15 = true)
{
boolean bool16 = bool15;
this.arithmeticProblemSolved = bool16;
RingerActivity localRingerActivity14 = this;
int i9 = 2131099787;
TextView localTextView4 = (TextView)localRingerActivity14.findViewById(i9);
this.problem = localTextView4;
RingerActivity localRingerActivity15 = this;
int i10 = 2131099788;
EditText localEditText1 = (EditText)localRingerActivity15.findViewById(i10);
this.solution = localEditText1;
RingerActivity localRingerActivity16 = this;
String str7 = "input_method";
InputMethodManager localInputMethodManager = (InputMethodManager)localRingerActivity16.getSystemService(str7);
this.imm = localInputMethodManager;
EditText localEditText2 = this.solution;
9 local91 = new com/splunchy/android/alarmclock/RingerActivity$9;
9 local92 = local91;
RingerActivity localRingerActivity17 = this;
ImageButton localImageButton5 = localImageButton3;
ImageButton localImageButton6 = localImageButton1;
local92.<init>(localRingerActivity17, localImageButton5, localImageButton6);
localEditText2.setOnEditorActionListener(local91);
...
…Ну что, добавляем перевод в верхний регистр строке, прочитанной из поля ввода, благо этому мы уже научились. После этого заменяем
iget-object v1, p0, Lcom/splunchy/android/alarmclock/RingerActivity$9;->this$0:Lcom/splunchy/android/alarmclock/RingerActivity;
invoke-static {v1}, Lcom/splunchy/android/alarmclock/RingerActivity;->access$10(Lcom/splunchy/android/alarmclock/RingerActivity;)Landroid/widget/EditText;
move-result-object v1
invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v1
invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v1
new-instance v2, Ljava/lang/Integer;
iget-object v3, p0, Lcom/splunchy/android/alarmclock/RingerActivity$9;->this$0:Lcom/splunchy/android/alarmclock/RingerActivity;
iget v3, v3, Lcom/splunchy/android/alarmclock/RingerActivity;->x:I
invoke-direct {v2, v3}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v2}, Ljava/lang/Integer;->toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
...
iget v3, v3, Lcom/splunchy/android/alarmclock/RingerActivity;->x:Iна
invoke-direct {v2, v3}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v2}, Ljava/lang/Integer;->toString()Ljava/lang/String;
move-result-object v2
iget v2, v3, Lcom/splunchy/android/alarmclock/RingerActivity;->x:Iи тут тоже добавляем перевод в верхний регистр. Собираем, смотрим. Ура! Все работает.
invoke-static {v2}, Ljava/lang/Integer;->toHexString(I)Ljava/lang/String;
move-result-object v2
Заключение
Скоро сказка сказывается, да не скоро дело делается. Из-за отсутствия дебаггера, тестирование очень сложно, для того чтобы запустить приложение и проверить его работоспособность требуется пара минут, и много надоедающих действий.
Также очень хотелось переделать цифровую раскладку моей любимой клавиатуры Swype для ввода в hex. Но их зашифрованные файлы раскладок стали для меня неодолимым препятствием. =(
Вот финальная версия будильника. К сожалению обновляться после модификации он перестал. Ну да мне и нынешнего функционала хватает.
Спасибо за внимание, буду рад любой критике!
P.S. Шестнадцатеричная таблица умножения