BruceFan's Blog

Stay hungry, stay foolish

0%

动态加载可以用来进行插件开发,这些插件大概都是为了在一个主程序中实现比较通用的功能,使主程序具有可扩展性。实现原理是实现一套插件接口,把插件实现编成apk或dex,在运行时用DexClassLoader动态加载进来。
预备知识:Android中的动态加载机制

插件演示

这里用了三个项目:

  • PInterface:插件接口项目(只是接口的定义)
  • PImplement:插件项目(实现插件接口,定义具体的功能)
  • HostProject:宿主项目(需要引用插件接口项目,然后动态加载插件项目)

项目介绍

下面看一下项目源代码:
1.PInterface项目

  1. IBean.java

    1
    2
    3
    4
    5
    6
    package com.pluginsdk.interfaces;

    public abstract interface IBean{
    public abstract String getName();
    public abstract void setName(String paramString);
    }
  2. IDynamic.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.pluginsdk.interfaces;

    import android.content.Context;

    public abstract interface IDynamic{
    public abstract void methodWithCallBack(YKCallBack paramYKCallBack);
    public abstract void showPluginWindow(Context paramContext);
    public abstract void startPluginActivity(Context context,Class<?> cls);
    public abstract String getStringForResId(Context context);
    }

    没有太多可说的,后面的代码可以下载项目之后自己看。

2.PImplement项目

  1. Dynamic.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package com.pluginsdk.pimplement;

    import android.app.AlertDialog;
    import android.app.AlertDialog.Builder;
    import android.app.Dialog;
    import android.content.Context;
    import android.content.DialogInterface;
    import android.content.Intent;

    import com.pluginsdk.R;
    import com.pluginsdk.bean.Bean;
    import com.pluginsdk.interfaces.IDynamic;
    import com.pluginsdk.interfaces.YKCallBack;

    public class Dynamic implements IDynamic{

    public void methodWithCallBack(YKCallBack callback) {
    Bean bean = new Bean();
    bean.setName("PLUGIN_SDK_USER");
    callback.callback(bean);
    }

    public void showPluginWindow(Context context) {
    AlertDialog.Builder builder = new Builder(context);
    builder.setMessage("对话框");
    builder.setTitle(R.string.hello_world);
    builder.setNegativeButton("取消", new Dialog.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    dialog.dismiss();
    }
    });
    Dialog dialog = builder.create();//.show();
    dialog.show();
    }

    public void startPluginActivity(Context context, Class<?> cls){
    /*
    * 这里要注意几点:
    * 1、如果单纯的写一个MainActivity的话,在主项目中也有一个MainActivity,开启的Activity还是主项目中的MainActivity
    * 2、如果这里将MainActivity写成全名的话,还是有问题,会报找不到这个Activity的错误
    */
    Intent intent = new Intent(context, cls);
    context.startActivity(intent);
    }

    public String getStringForResId(Context context){
    return context.getResources().getString(R.string.hello_world);
    }

    }
  2. Bean.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.pluginsdk.bean;


    public class Bean implements com.pluginsdk.interfaces.IBean{
    private String name = "这是来自于插件项目中设置的初始化的名字";

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    }

3.宿主项目HostProject

  1. MainActivity.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    package com.plugindemo;
    import java.io.File;
    import java.lang.reflect.Method;

    import android.annotation.SuppressLint;
    import android.app.Activity;
    import android.content.Context;
    import android.content.res.AssetManager;
    import android.content.res.Resources;
    import android.content.res.Resources.Theme;
    import android.os.Bundle;
    import android.os.Environment;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.ListView;
    import android.widget.Toast;

    import com.pluginsdk.interfaces.IBean;
    import com.pluginsdk.interfaces.IDynamic;
    import com.pluginsdk.interfaces.YKCallBack;

    import dalvik.system.DexClassLoader;

    public class MainActivity extends Activity {
    private AssetManager mAssetManager;//资源管理器
    private Resources mResources;//资源
    private Theme mTheme;//主题
    private String apkFileName = "Pimplement.apk";
    private String dexpath = null;//apk文件地址
    private File fileRelease = null; //释放目录
    private DexClassLoader classLoader = null;
    @SuppressLint("NewApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    dexpath = getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + apkFileName;
    fileRelease = getDir("dex", 0);

    /*初始化classloader
    * dexpath dex文件地址
    * fileRelease 文件释放地址
    * 父classLoader
    */
    classLoader = new DexClassLoader(dexpath, fileRelease.getAbsolutePath(), null, getClassLoader());

    Button btn_1 = (Button)findViewById(R.id.btn_1);
    Button btn_2 = (Button)findViewById(R.id.btn_2);
    Button btn_3 = (Button)findViewById(R.id.btn_3);
    Button btn_4 = (Button)findViewById(R.id.btn_4);
    Button btn_5 = (Button)findViewById(R.id.btn_5);
    Button btn_6 = (Button)findViewById(R.id.btn_6);

    btn_1.setOnClickListener(new View.OnClickListener() {//普通调用 反射的方式
    @Override
    public void onClick(View v) {
    Class mLoadClassBean;
    try {
    mLoadClassBean = classLoader.loadClass("com.pluginsdk.bean.Bean");
    Object beanObject = mLoadClassBean.newInstance();
    Log.d("DEMO", "ClassLoader:" + mLoadClassBean.getClassLoader());
    Log.d("DEMO", "ClassLoader:" + mLoadClassBean.getClassLoader().getParent());
    Method getNameMethod = mLoadClassBean.getMethod("getName");
    getNameMethod.setAccessible(true);
    String name = (String) getNameMethod.invoke(beanObject);
    Toast.makeText(MainActivity.this, name, Toast.LENGTH_SHORT).show();
    } catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }
    }
    });
    btn_2.setOnClickListener(new View.OnClickListener() {//带参数调用
    @Override
    public void onClick(View arg0) {
    Class mLoadClassBean;
    try {
    mLoadClassBean = classLoader.loadClass("com.pluginsdk.bean.Bean");
    Object beanObject = mLoadClassBean.newInstance();
    //接口形式调用
    Log.d("DEMO", beanObject.getClass().getClassLoader()+"");
    Log.d("DEMO",IBean.class.getClassLoader()+"");
    Log.d("DEMO",ClassLoader.getSystemClassLoader()+"");
    IBean bean = (IBean)beanObject;
    bean.setName("宿主程序设置的新名字");
    Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
    }catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }

    }
    });
    btn_3.setOnClickListener(new View.OnClickListener() {//带回调函数的调用
    @Override
    public void onClick(View arg0) {
    Class mLoadClassDynamic;
    try {
    mLoadClassDynamic = classLoader.loadClass("com.pluginsdk.pimplement.Dynamic");
    Object dynamicObject = mLoadClassDynamic.newInstance();
    //接口形式调用
    IDynamic dynamic = (IDynamic)dynamicObject;
    //回调函数调用
    YKCallBack callback = new YKCallBack() {//回调接口的定义
    public void callback(IBean arg0) {
    Toast.makeText(MainActivity.this, arg0.getName(), Toast.LENGTH_SHORT).show();
    };
    };
    dynamic.methodWithCallBack(callback);
    } catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }

    }
    });
    btn_4.setOnClickListener(new View.OnClickListener() {//带资源文件的调用
    @Override
    public void onClick(View arg0) {
    loadResources();
    Class mLoadClassDynamic;
    try {
    mLoadClassDynamic = classLoader.loadClass("com.pluginsdk.pimplement.Dynamic");
    Object dynamicObject = mLoadClassDynamic.newInstance();
    //接口形式调用
    IDynamic dynamic = (IDynamic)dynamicObject;
    dynamic.showPluginWindow(MainActivity.this);
    } catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }
    }
    });
    btn_5.setOnClickListener(new View.OnClickListener() {//带资源文件的调用
    @Override
    public void onClick(View arg0) {
    loadResources();
    Class mLoadClassDynamic;
    try {
    mLoadClassDynamic = classLoader.loadClass("com.pluginsdk.pimplement.Dynamic");
    Object dynamicObject = mLoadClassDynamic.newInstance();
    //接口形式调用
    IDynamic dynamic = (IDynamic)dynamicObject;
    dynamic.startPluginActivity(MainActivity.this,
    classLoader.loadClass("com.plugindemo.MainActivity"));
    } catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }
    }
    });
    btn_6.setOnClickListener(new View.OnClickListener() {//带资源文件的调用
    @Override
    public void onClick(View arg0) {
    loadResources();
    Class mLoadClassDynamic;
    try {
    mLoadClassDynamic = classLoader.loadClass("com.pluginsdk.pimplement.Dynamic");
    Object dynamicObject = mLoadClassDynamic.newInstance();
    //接口形式调用
    IDynamic dynamic = (IDynamic)dynamicObject;
    String content = dynamic.getStringForResId(MainActivity.this);
    Toast.makeText(getApplicationContext(), content+"", Toast.LENGTH_LONG).show();
    } catch (Exception e) {
    Log.e("DEMO", "msg:"+e.getMessage());
    }
    }
    });

    }

    protected void loadResources() {
    try {
    AssetManager assetManager = AssetManager.class.newInstance();
    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
    addAssetPath.invoke(assetManager, dexpath);
    mAssetManager = assetManager;
    } catch (Exception e) {
    e.printStackTrace();
    }
    Resources superRes = super.getResources();
    superRes.getDisplayMetrics();
    superRes.getConfiguration();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
    mTheme = mResources.newTheme();
    mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
    return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources() {
    return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Theme getTheme() {
    return mTheme == null ? super.getTheme() : mTheme;
    }
    }

项目引用关系

1.将接口项目PInterface设置成一个Library,

并导出项目为一个jar。

2.插件项目PImplement引用接口项目的jar
注意是lib文件夹,而不是libs,在Android中的动态加载机制
中说过这样做的原因:插件项目打包不能集成接口jar,宿主项目打包一定要集成接口jar

3.HostProject项目引用PInterface这个Library

项目引用完成后,我们编译PImplement项目,生成PImplement.apk放到/data/data/com.plugindemo/files,因为代码中是从这个目录进行加载的,这个目录是可以修改的。运行HostProject

运行成功,这个对话框其实是在插件中定义的。
项目下载
reference
http://blog.csdn.net/jiangwei0910410003/article/details/41384667

忘了说我的运行环境是Ubuntu14.04,vim,gcc version 4.8.4,操作系统和编辑器没什么要紧,gcc如果版本低了可能不支持C++11。
编译运行方法如下:

1
2
$ g++ -std=c++11 -o test test.c
$ ./test

标准库类型vector

标准库类型vector(也常被称为容器)表示对象的集合,其中所有对象的类型都相同。集合中每个对象都有一个与之对应的索引,索引用于访问对象。想要使用vector,必须包含头文件。

1
2
#include <vector>
using std::vector;

C++语言既有类模板(class template),也有函数模板。编译器根据模板创建类或函数的过程称为实例化(instantiation)。模板名字后面跟一对尖括号,在括号内放上存放对象的类型。

1
2
3
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象

定义vector对象

1
2
3
vector<int> ivec; // 默认初始化为空
vector<int> ivec2(ivec); // 把ivec的元素拷贝给ivec2
vector<int> ivec3 = ivec; // 把ivec的元素拷贝给ivec3

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

列表初始化vector对象

1
2
vector<string> svec{"a", "an", "the"}; // ivec包含了三个元素
vector<int> ivec = {2, 3, 4, 5} // ivec包含了2,3,4,5四个元素

创建指定数量的元素

1
vector<int> ivec(10, -1); // ivec包含了10个-1

向vector对象中添加元素

vector的成员函数push_back可以向其中添加元素,push_back负责把一个值当成vector对象的尾元素压到vector对象的尾端。

1
2
3
4
5
6
7
8
9
vector<int> v2; // 空
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 依次把i添加到v2尾端

sting word;
vector<string> text; // 空
while (cin >> word) {
text.push_back(word); // 把word添加到text后面
}

其他vector操作

操作 含义
v.empty() 如果v不含任何元素返回真;否则返回假
v.size() 返回v中元素的个数
v[n] 返回v中第n个位置上元素的引用
v1 == v2 v1和v2对应位置的元素都相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <vector>
using std::vector;
using std::cout;
using std::endl;
int main()
{
vector<int> v{1,2,3,4,5,6,7,8,9};
for (auto &i : v) // 这里i为引用
i *= i; // 求元素的平方
for (auto i : v)
cout << i << " "; // 输出容器中的元素
cout << endl;
return 0;
}

迭代器

除了下标运算符可以访问string对象或vector对象的元素,迭代器(iterator)也可以。除了vector,标准库还定义了其他几种容器,所有标准库容器都可以使用迭代器。
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。

1
2
// b表示v的第一个元素的位置,e表示v尾元素的下一个位置
auto b = v.begin(), e = v.end();

end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)

如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。

迭代器运算符

运算符 含义
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter 令iter指示容器中的下一个元素
–iter 令iter指示容器中的上一个元素
iter1 == iter2 判断两个迭代器是否相等
iter1 != iter2 判断两个迭代器是否不相等

和指针类似,也能通过解引用迭代器来获取他所指示的元素。

1
2
3
// 依次处理s的字符直至我们处理完全部字符或遇到空格
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it);

迭代器类型

拥有迭代器的标准库类型使用iteratorconst_iterator来表示迭代器的类型:

1
2
3
4
5
6
7
8
9
10
vector<int>::iterator it; // it能读写vector<int>的元素
for (it = v.begin(); it != v.end(); ++i) {
*it *= *it; // 正确,可以读写元素
cout << *it << " ";
}
vector<int>::const_iterator cit; // cit只能读vector<int>的元素,不能写
for (cit = v.begin(); it != v.end(); ++i) {
*cit *= *cit; // 错误,只能读不能写
cout << *cit << " ";
}

如果vector对象是一个常量(const vector<int>),只能用const_iterator;如果不是常量,则iterator和const_iterator都可以用。C++11新标准引入了cbegin()cend()两个新函数,不论vector对象是否是常量,返回值都是const_vector。

迭代器运算

1
2
3
4
// 计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin + v.size()/2;
if (it < mid)
// 处理vi前半部分的元素

迭代器实现二分搜索

1
2
3
4
5
6
7
8
9
10
11
// text必须是有序的,beg和end表示我们搜索的范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg) / 2; // 初始状态下的中间点
// 当还有元素尚未检查并且我们还没有找到sought时执行循环
while (mid != end && *mid != sought) {
if (sought < *mid)
end = mid;
else
beg = mid + 1;
mid = beg + (end - beg) / 2; // 新的中间点
}

下面的方法是基于编译完Android系统源码之后,再另外下载Linux内核进行编译调试的。我在编译Android6.0.1源码,烧录到nexus5真机上这篇文章中编译的是lunch 8,即aosp_hammerhead-userdebug,但是用emulator启动模拟器时,需要用模拟器版本的编译lunch 1,即aosp_arm-eng。

下载Android内核源码

1
2
3
$ cd /Computer/Android/android4.4.2/kernel
$ git clone https://android.googlesource.com/kernel/goldfish.git
$ cd goldfish

Goldfish是一种虚拟的ARM处理器,在android的仿真环境中使用。
查看支持哪些Linux内核版本的下载:

1
$ git branch -a

选择3.4内核版本进行下载:

1
$ git checkout -t remotes/origin/android-goldfish-3.4 -b goldfish3.4

分支goldfish3.4设置为跟踪来自origin的远程分支android-goldfish-3.4,切换到一个新分支goldfish3.4

开始编译

1
2
3
4
5
6
7
$ make menuconfig # 到Kernel hacking中打开Compile the kernel with debug info、Enable dynamic printk() support、KGDB
$ make ARCH=arm goldfish_armv7_defconfig
$ make ARCH=arm SUBARCH=arm CROSS_COMPILE=/home/fanrong/Computer/Android/android4.4.2/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/bin/arm-eabi-

... // 一路Enter
OBJCOPY arch/arm/boot/zImage
Kernel: arch/arm/boot/zImage is ready

我编译的时候出过一些错误,也不知道怎么解决,MD再编译一遍错误就没有了,编译了3遍就成功了,还好内核源码编译比系统源码编译快。

在模拟器中运行编译的内核

1
2
3
4
5
$ cd ../..
$ source build/envsetup.sh
$ lunch
[aosp_arm-eng]
$ emulator -debug init -kernel kernel/goldfish/arch/arm/boot/zImage -system out/target/product/generic/system.img -ramdisk out/target/product/generic/ramdisk.img

用已编译的内核启动Android模拟器

调试Android内核

首先要在/tmp文件夹下创建一个qemu文件夹,再在qemu里创建Socket文件:

1
2
3
$ cd /tmp; mkdir qemu; cd qemu; touch Socket
# 回到Android源码根目录
$ emulator -debug init -kernel kernel/goldfish/arch/arm/boot/zImage -system out/target/product/generic/system.img -ramdisk out/target/product/generic/ramdisk.img -qemu -monitor unix:/tmp/qemu/Socket,server,nowait -s

配置一个环境变量,在PATH中加入arm-linux-androideabi-gdb所在路径:

1
/home/fanrong/Computer/Android/android4.4.2/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7/bin

另开一个终端:

1
2
3
4
5
6
7
# 到goldfish目录中
$ arm-linux-androideabi-gdb vmlinux
...
(gdb) target remote :1234
Remote debugging using :1234
0xc00155c8 in cpu_v7_do_idle ()
(gdb)

reference
https://github.com/Fuzion24/AndroidKernelExploitationPlayground

复合类型

引用

引用(reference)为对象起了另外一个名字。

1
2
3
int ival = 1024;
int &refVal = ival; // refVal指向ival
int &refVal2; // 报错:引用必须被初始化

定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用即别名 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:

1
refVal = 2; // 把2赋值给ival

指针

指针与引用类似,也实现了对其它对象的间接访问。不同于引用的是:

  • 指针本身是一个对象,允许对其赋值和拷贝,生命周期内它可以指向几个不同的对象。
  • 指针无需在定义时赋初值。

获取对象的地址
指针存放某个对象的地址,获取该地址需要取地址符 &

1
2
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针

利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符 *来访问该对象。

1
cout << *p; // 由符号*得到指针p所指的对象,输出42

空指针
空指针不指向任何对象。C++11初始化空指针的方法:

1
int *p = nullptr;

const限定符

关键字const定义常量,const对象一旦创建后其值就不能再改变了,所以const对象必须初始化。

1
const int bufSize = 512;

const的引用

可以把引用绑定到const对象上,我们称之为对常量的引用,对常量的引用不能被用作修改它所绑定的对象。

1
2
const int ci = 1024;
const int &r1 = ci; // 引用及其对应的对象都是常量

指针和const

指向常量的指针(pointer to const)不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。

1
2
const double pi = 3.14;
const double *cptr = &pi; // cptr可以指向一个双精度常量

指针是对象而引用不是,允许把指针本身定为常量。常量指针(const pointer)必须初始化,*放在const关键字之前用以说明指针是一个常量,即不变的是指针本身的值而非指向的那个值。

1
2
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb

顶层const

用名词顶层const表示指针本身是个常量,而用名词底层const表示指针所指的对象是一个常量。

1
2
3
4
5
int i = 0;
int *const p1 = &i; // 不能改变p1的值,顶层const
const int ci = 42; // 不能改变ci的值,顶层const
const int *p2 = &ci; // 可以改变p2的值,底层const
const int &r = ci; // 用于声明引用的const都是底层const

处理类型

类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。有两种方法定义类型别名:

  • 传统方法是用关键字typedef

    1
    2
    3
    4
    5
    typedef double wages; // wages是double的同义词
    typedef wages base, *p; // base是double的同义词,p是double*的同义词

    typedef unsigned long (* _prepare_kernel_cred)(unsigned long cred);
    _prepare_kernel_cred prepare_kernel_cred;

    以上两句就相当于:unsigned long (*prepare_kernel_cred)(unsigned long cred);
    定义了一个函数指针prepare_kernel_cred,函数参数为(unsigned long cred),返回值为unsigned long。
    建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头。

  • 新标准使用别名声明(alias declaration)

    1
    using SI = Sales_item; // SI是Sales_item的同义词

    这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。

auto类型说明符

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候就清楚地知道表达式的类型。然而有时做不到这样,因此,C++11新标准引入了auto类型说明符,它能让编译器替我们去分析表达式所属的类型,让编译器通过初始值来推算变量的类型。auto定义的变量必须有初始值。

1
2
3
auto item = val1 + val2; // item初始化为val1和val2相加的结果
auto i = 0, *p = &i; // 正确:i是整数、p是整形指针
auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致

decltype类型指示符

有时希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

1
2
3
4
5
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int &, y绑定到变量x
decltype(cj) z; // 错误:z是一个引用,必须初始化

本文主要介绍Android动态加载jar的技术,如何开发一个可以自定义控件的Android应用?就像eclipse一样,可以动态加载插件;如何让Android应用执行服务器上的不可预知的代码?如何对Android应用加密,而只在执行时自解密,从而防止被破解?这篇文章是一个基础,后面的一些文章会以此继续深入。

类加载机制

Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流,因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。
然而Dalvik虚拟机毕竟不算是标准的Java虚拟机,因此在类加载机制上,它们有相同的地方,也有不同之处。例如,在使用标准Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。然后通过defineClass方法来从一个二进制流中加载Class,然而,这在Android里是行不通的。参看源码我们知道,Android中ClassLoader的defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。而这个本地方法除了抛出一个UnsupportedOperationException之外,什么都没做,甚至连返回值都为空。
代码清单 /dalvik/vm/native/java_lang_VMClassLoader.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Dalvik_java_lang_VMClassLoader_defineClass(const u4* args, JValue* pResult) {  
Object* loader = (Object*) args[0];
StringObject* nameObj = (StringObject*) args[1];
const u1* data = (const u1*) args[2];
int offset = args[3];
int len = args[4];
Object* pd = (Object*) args[5];
char* name = NULL;
name = dvmCreateCstrFromString(nameObj);
LOGE("ERROR: defineClass(%p, %s, %p, %d, %d, %p)\n",loader, name, data, offset, len, pd);
dvmThrowException("Ljava/lang/UnsupportedOperationException;","can't load this type of class file");
free(name);
RETURN_VOID();
}

Dalvik虚拟机类加载机制

那如果在Dalvik虚拟机里,ClassLoader不好使,我们如何实现动态加载类呢?Android为我们从ClassLoader派生出了两个类:DexClassLoader(/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java)和PathClassLoader(/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java)。这两个继承自ClassLoader的类加载器,本质上是重载了ClassLoader的findClass方法。在执行loadClass时,我们可以参看ClassLoader部分源码:
代码清单 /libcore/libdvm/src/main/java/java/lang/ClassLoader.java: loadClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {
clazz = findClass(className);
}
}
return clazz;
}

因此DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是说,它们在加载一个类之前,回去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。
DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。这里需要顺便提一下的是,Dalvik虚拟机识别的是dex文件,而不是class文件。因此,我们供类加载的文件也只能是dex文件,或者包含有dex文件的apk或jar文件。
PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过其静态方法loadDex(path, outpath, 0)得到DexFile对象。
PathClassLoader是Android应用中的默认加载器。这两者的区别在于:

  • DexClassLoader可以加载任意路径的apk、jar和dex文件,并且会在指定的outpath路径释放出dex文件。
  • PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认类加载器的原因,因为一般程序都是安装了再打开。

另外,PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName,而DexClassLoader调用的是loadClass。因此,在使用PathClassLoader时类全名需要用/替换.
下面看一下Android中的各种类加载器分别加载哪些类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.androiddemo;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// java.lang.BootClassLoader,也继承了ClassLoader类
Log.i("DEMO", "Context的类加载加载器:"+Context.class.getClassLoader());
Log.i("DEMO", "ListView的类加载器:"+ListView.class.getClassLoader());
// dalvik.system.PathClassLoader
Log.i("DEMO", "应用程序默认加载器:"+getClassLoader());
Log.i("DEMO", "系统类加载器:"+ClassLoader.getSystemClassLoader());
// 默认加载器PathClassLoader的父亲是BootClassLoader
Log.i("DEMO","打印应用程序默认加载器的委派机制:");
ClassLoader classLoader = getClassLoader();
while(classLoader != null){
Log.i("DEMO", "类加载器:"+classLoader);
classLoader = classLoader.getParent();
}
// 系统加载器PathClassLoader的父亲也是BootClassLoader
Log.i("DEMO","打印系统加载器的委派机制:");
classLoader = ClassLoader.getSystemClassLoader();
while(classLoader != null){
Log.i("DEMO", "类加载器:"+classLoader);
classLoader = classLoader.getParent();
}
}
}

实际操作

使用到的工具都比较常规:javac、dx、eclipse等其中dx工具最好是指明–no-strict,因为class文件的路径可能不匹配。
加载好类后,通常我们可以通过Java反射机制来使用这个类,但是这样效率相对不高,而且老用反射代码也比较复杂凌乱。更好的做法是定义一个interface,并将这个interface写进宿主程序里。待加载的类,继承自这个interface,并且有一个参数为空的构造函数,以使我们能够通过Class的newInstance方法产生对象然后将对象强制转换为interface对象,于是就可以直接调用成员方法了,下面是具体的实现步骤了:
第一步
编写好动态代码类:

代码清单 IDynamic.java

1
2
3
4
5
6
7
8
9
10
11
package com.dynamic.interfaces;
import android.app.Activity;

public interface IDynamic {
public void init(Activity activity);
public void showBanner();
public void showDialog();
public void showFullScreen();
public void showAppWall();
public void destroy();
}

代码清单 Dynamic.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.dynamic.impl;
import android.app.Activity;
import android.widget.Toast;
import com.dynamic.interfaces.IDynamic;

// 动态类实现
public class Dynamic implements IDynamic {
private Activity mActivity;

@Override
public void init(Activity activity) {
mActivity = activity;
}

@Override
public void showBanner() {
Toast.makeText(mActivity, "我是ShowBanner方法", 1500).show();
}

@Override
public void showDialog() {
Toast.makeText(mActivity, "我是ShowDialog方法", 1500).show();
}

@Override
public void showFullScreen() {
Toast.makeText(mActivity, "我是ShowFullScreen方法", 1500).show();
}

@Override
public void showAppWall() {
Toast.makeText(mActivity, "我是ShowAppWall方法", 1500).show();
}

@Override
public void destroy() {

}
}

第二步
将上面开发好的动态类打包成jar,这里要注意的是只打包实现类Dynamic.java,不打包接口类IDynamic.java
右键项目->Export

点击next

然后使用dx命令:(我的jar文件是dynamic.jar)
dx --dex --output=dynamic_temp.jar dynamic.jar
这样就生成了dynamic_temp.jar,这个jar和dynamic.jar有什么区别呢?
其实这条命令主要做的工作是:首先将dynamic.jar编译成dynamic.dex文件(Android虚拟机认识的字节码文件),然后再将dynamic.dex文件压缩成dynamic_temp.jar,当然你也可以压缩成zip格式的,或者直接编译成apk文件都可以的,这个后面会说到。
同样的方法只打包接口类IDynamic.java,得到dynamic_int.jar,不用dx处理,第三步中会用到。
第三步
新建一个DynamicDemo项目,目录如下

下面看一下目标类:
代码清单 AndroidDynamicLoadClassActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.dynamic.demo;
import java.io.File;
import java.util.List;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import com.dynamic.interfaces.IDynamic;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class AndroidDynamicLoadClassActivity extends Activity {
// 动态类加载接口
private IDynamic lib;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化组件
Button showBannerBtn = (Button) findViewById(R.id.show_banner_btn);
Button showDialogBtn = (Button) findViewById(R.id.show_dialog_btn);
Button showFullScreenBtn = (Button) findViewById(R.id.show_fullscreen_btn);
Button showAppWallBtn = (Button) findViewById(R.id.show_appwall_btn);

/* 使用DexClassLoader方式加载类 */
// dex压缩文件的路径(可以是apk,jar,zip格式)
String jarPath = getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + "dynamic_temp.jar";
// dex解压释放后的目录
String dexOutputDirs = getApplicationContext().getCacheDir().getAbsolutePath();
// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
DexClassLoader cl = new DexClassLoader(jarPath, dexOutputDirs, null, getClassLoader());

/* 使用PathClassLoader方法加载类 */
// 创建一个intent,用来找到指定的apk:这里的"com.dynamic.impl"是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.dynamic.impl"/>
Intent intent = new Intent("com.dynamic.impl", null);
// 获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
// 获得指定的activity信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
// 获得apk的目录或者jar的目录
String apkPath = actInfo.applicationInfo.sourceDir;
// native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
// 创建类加载器,把dex加载到虚拟机中
// 第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取
// 第二个参数:是C/C++依赖的本地库文件目录,可以为null
// 第三个参数:是上一级的类加载器
PathClassLoader pcl = new PathClassLoader(apkPath, libPath, this.getClassLoader());

// 加载类
try {
// com.dynamic.impl.Dynamic是动态类名
// 使用DexClassLoader加载类
Class libProviderClazz = cl.loadClass("com.dynamic.impl.Dynamic");
// 使用PathClassLoader加载类
// Class libProviderClazz = pcl.loadClass("com.dynamic.impl.Dynamic");
lib = (IDynamic)libProviderClazz.newInstance();
if (lib != null) {
lib.init(AndroidDynamicLoadClassActivity.this);
}
} catch (Exception exception) {
exception.printStackTrace();
}
// 下面分别调用动态类中的方法
showBannerBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showBanner();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showDialogBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showDialog();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showFullScreenBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showFullScreen();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showAppWallBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showAppWall();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
}
}

这里定义了一个IDynamic接口变量,而且给出了DexClassLoader和PathClassLoader加载类的使用方法。

1
DexClassLoader cl = new DexClassLoader(jarPath, dexOutputDirs, null, getClassLoader());

DexClassLoader是继承ClassLoader类的,这里面的参数说明:

  • 第一个参数是:dex压缩文件的路径,就是我们将上面编译后的dynamic_temp.jar存放的目录,当然也可以是zip和apk格式的。

  • 第二个参数是:dex解压后存放的目录,就是将jar、zip或apk文件解压出的dex文件存放的目录,这个就和PathClassLoader方法有区别了,同时你也可以看到PathClassLoader方法中没有这个参数,这个也真是这两个类的区别:
    PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在手机的data/dalvik目录中存在缓存的dex文件)。而DexClassLoader可以支持apk、jar和dex文件,并且会在指定的outpath路径释放出dex文件。

    我们可以通过DexClassLoader方法指定解压后的dex文件的存放目录,但是我们一般不这么做,因为这样做无疑的暴露了dex文件,所以我们一般不会将jar/zip/apk压缩文件存放到用户可以察觉到的位置,同时解压dex的目录也是不能让用户看到的。

  • 第三个参数是:加载的时候需要用到的lib库,这个一般不用。

  • 第四个参数是:给DexClassLoader指定父加载器。

com.dynamic.interfaces这个包是从Dynamic项目中剪切过来的,同时要在Dynamic项目中新建一个lib文件夹,将dynamic_int.jar文件拷进去,然后右键dynamic_int.jar->Build Path->Add to Build Path,后面会说为什么要这么做。
第四步
运行目标类:
如果用的是DexClassLoader方式加载类:这时候需要将jar或者zip或者apk文件放到指定的目录中,我这里放到了/data/data/com.dynamic.demo/files目录。
如果用的是PathClassLoader方法加载类:这时候需要先将Dynamic.apk安装到设备上,不然找不到这个指定的activity,同时需要注意:

1
Intent intent = new Intent("com.dynamic.impl", null);

这里com.dynamic.impl是一个action,需要定义在Dynamic项目中,这个名称是动态apk和目标apk之间约定好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".DynamicActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="com.dynamic.impl" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

运行结果:

点击按钮就弹出对应的Toast,成功地运行了动态类中的代码。
其实更好的办法就是将动态的jar,zip,apk文件从网络上获取,安全可靠,同时本地的目标项目不需要改动代码就可以执行不同的逻辑了。

问题解释

需要解释的是项目中接口和jar的位置以及导入方式,在解释原因之前先来了解一下Eclipse中引用项目的不同方式和区别:
第一种 最常用的将引用项目打成jar放到需要引用项目的libs下面(这里是将PluginImpl打成jar,放到HostProject项目的libs中)
这种方式是Eclipse推荐使用的,当我们在建立一个项目的时候也会自动产生这个文件夹,当我们将我们需要引用的项目打成jar,然后放到这个文件夹之后,Eclipse就自动导入了(这个功能是Eclipse3.7之后有的)。
第二种 和第一种的区别是,我们可以从新新建一个文件夹比如是lib,然后将引用的jar放到这个文件夹中,但是此时Eclipse是不会自动导入的,需要我们手动的导入(Add to Build Path…),但是这个是一个区别,还有一个区别,也是到这个这个报错原因的区别,就是libs文件夹中的jar,在运行的时候是会将这个jar集成到程序中的,而我们新建的文件夹(名字非libs即可),及时我们手动的导入,编译是没有问题的,但是运行的时候,是不会将jar集成到程序中。
第三种 和前两种的区别是不需要将引用项目打成jar,直接引用这个项目

这种方式其实效果和第一种差不多,唯一的区别就是不需要打成jar,但是运行的时候是不会将引用项目集成到程序中的。
第四种 和第三种的方式是一样的,也是不需要将引用项目打成jar,直接引用项目:

这个前提是需要设置引用项目为Library,同时引用的项目和被引用的项目必须在一个工作空间中,不然会报错,这种的效果和第二种是一样的,在运行的时候是会将引用项目集成到程序中的。
第五种 和第一种、第二种差不多,导入jar:

这里有很多种方式选择jar的位置,但是这些操作的效果和第一种是一样的,运行的时候是不会将引用的jar集成到程序中的。
上面两个项目可以将Dynamic看成一个插件项目,DynamicDemo看成一个宿主项目,要遵循的一个原则就是插件项目不能集成接口,宿主项目一定要集成接口
项目下载
reference
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
http://blog.csdn.net/jiangwei0910410003/article/details/41384667

运行时间
评估算法的性能。首先,要计算各个排序算法在不同的随机输入下的基本操作的次数(包括比较和交换,或者是读写数组的次数)。然后,我们用这些数据估计算法的相对性能。
额外的内存使用
排序算法的额外内存开销和运行时间是同样重要的。排序算法可分为两类:除了函数调用所需的栈和固定数目的实例变量之外无需额外内存的原地排序算法,以及需要额外内存空间来存储另一份数组副本的其他排序算法。
排序算法类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Example {
public static void sort(Comparable[] a) {

}

private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}

private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i]; a[i] = a[j]; a[j] = t;
}

private static void show(Comparable[] a) {
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}

public static boolean isSorted(Comparable[] a) {
// 测试元素是否有序
for (int i = 0; i < a.length; i++) {
if (less(a[i], a[i-1])) return false;
}
return true;
}

public static void main(String[] args) {
String[] a = {"S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"};
sort(a);
show(a);
}
}

这个模板适用于任何实现了Comparable接口的数据类型。很多希望排序的数据都实现了Comparable接口。如Java中封装数字的类型Integer和Double,以及String和其他许多高级数据类型都实现了Comparable接口。在创建自己的数据类型时,只要实现Comparable接口就能够保证用例代码可以将其排序。

选择排序

  1. 找到数组中最小的元素,将它和数组的第一个元素交换位置。
  2. 在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。
  3. 如此往复,直至整个数组排序。

两个特点

  • 运行时间和输入无关。
  • 数据移动是最少的。
1
2
3
4
5
6
7
8
9
10
public static void selectionSort(Comparable[] a) {
int n = a.length;
for (int i = 0; i < n; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (less(a[j], a[min])) min = j;
exch(a, i, min);
}
}
}

插入排序

将每一个元素插入到其他已经有序元素中的适当位置,为了给要插入的元素腾出空间,需要将其余所有元素在插入之前都向右移动一位。
对于某些类型的非随机数组很有效。

1
2
3
4
5
6
7
8
public static void insertionSort(Comparable[] a) {
// 将a按升序排列
int n = a.length;
for (int i = 1; i < n; i++) { // 将a[i]插入到a[i-1]、a[i-2]、a[i-3]...之中
for (int j = i; j > 0 && less(a[j], a[j-1]); j--)
exch(a, j, j - 1);
}
}

希尔排序

希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。
实现方法:因为子数组是相互独立的,一种简单的方法是在h子数组中将每个元素交换到比它大的元素之前去。只需要在插入排序的代码中将移动元素的距离由1改为h即可。这样,希尔排序的实现就转化为了一个类似于插入排序但使用不同增量的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void shellSort(Comparable[] a) {
// 将a[]按升序排列
int N = a.length;
int h = 1;
while (h < N/3) h = 3 * h + 1; // 1, 4, 13, 40, 121...
while (h >= 1) { // 将数组变为h有序
for (int i = h; i < N; i++) { // 将a[i]插入到a[i-h],a[i-2*h],a[i-3*h]...之中
for (int j = i; j >= h && less(a[j], a[j-h]); j -= h)
exch(a, j, j-h);
}
h = h / 3;
}
}

基于线程的并发编程

线程(thread)就是运行在进程上下文中的逻辑流。线程由内核自动调度。每个线程都有它自己的线程上下文(thread context),包括一个唯一的整数线程ID(Thread ID,TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间。

Posix线程

Posix线程(Pthreads)是在C程序中处理线程的一个标准接口。下面的Pthreads程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *thread(void *vargp);

int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread, NULL); // 返回时,主线程和对等线程同时运行
pthread_join(tid, NULL); // 等待对等线程终止
exit(0);
}

void *thread(void *vargp)
{
printf("Hello, world!\n");
return NULL;
}

主线程(main thread)创建一个对等线程(peer thread),然后等待它的终止。对等线程输出”Hello, world!\n”并终止。当主线程检测到对等线程终止后,他就通过调用exit终止该进程。
如第二行里的原型所示,每个线程例程都以一个通用指针作为输入,并返回一个通用指针。如果想传递多个参数给线程例程,那么应该将参数放到一个结构中,并传递一个指向该结构的指针。

1.写程序时忘记函数所需的头文件可以用man来查看,如pthread_create函数可以用man pthread_create命令查看所需头文件。
2.该文件编译:gcc -o hello hello.c -lpthread ,pthread不是Linux下的默认的库,也就是在链接的时候,无法找到pthread库中函数的入口地址,于是链接会失败。

创建线程

线程通过调用pthread_create函数来创建其他线程。

1
2
3
4
#include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid, pthread_attr_t *attr,
func *f, void *arg);

返回:若成功则返回0,若出错则为非零。
pthread_create函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。能用attr参数来改变新创建线程的默认属性。
新线程可以通过调用pthread_self函数来获得它自己的线程ID。

1
2
#include <pthread.h>
pthread_t pthread_self(void);

终止线程

终止方式如下:

  • 当顶层的线程例程返回时,线程会隐式地终止。
  • 通过调用pthread_exit函数,线程会显式地终止。如果主线程调用pthread_exit,它会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回值为thread_return。
    1
    2
    #include <pthread.h>
    void pthread_exit(void *thread_return);
  • 某个对等线程调用Unix的exit函数,该函数终止进程以及所有与改进程相关的线程。
  • 另一个对等线程通过以当前线程ID作为参数调用pthread_cancle函数来终止当前线程。
    1
    2
    #include <pthread.h>
    int pthread_cancel(pthread_t tid);

回收已终止线程的资源

线程通过调用pthread_join函数等待其他线程终止。

1
2
#include <pthread.h>
int pthread_join(pthread_t tid, void **thread_return);

pthread_join函数会阻塞,直到线程tid终止,将线程例程返回的(void *)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源。
和Unix的wait函数不同,pthread_join函数只能等待一个指定的线程终止。没有办法让pthread_join等待任意某个线程终止。

分离线程

在任一个时间点,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能被其他线程收回其资源和杀死。一个分离的线程是不能被其他线程回收或杀死的。
线程默认是可结合的,为了避免存储器泄露,每个可结合线程都应该要么被其他线程显式回收,要么通过pthread_detach函数被分离。

1
2
#include <pthread.h>
int pthread_detach(pthread_t tid);

初始化线程

1
2
3
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

once_control变量是一个全局或者静态变量,总是被初始化为PTHREAD_ONCE_INIT。第一次用参数once_control调用pthread_once时,它调用init_routine,这是一个没有输入参数,也没有返回的函数。(后面有应用)

一个基于线程的并发服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "csapp.h"
void echo(int connfd);
void *thread(void *vargp);

int main(int argc, char **argv)
{
int listenfd, *connfdp, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
pthread_t tid;

if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
listenfd = Open_listenfd(port);
while (1) {
// 为了避免对等线程的赋值语句和主线程的accept语句间引入的竞争
connfdp = Malloc(sizeof(int));
*connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen);
Pthread_create(&tid, NULL, thread, connfdp);
}
}

void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;

Rio_readinitb(&rio, connfd);
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", n);
Rio_writen(connfd, buf, n);
}
}

void *thread(void *vargp)
{
int connfd = *((int *)vargp);
Pthread_detach(pthread_self());
Free(vargp);
echo(connfd);
Close(connfd);
return NULL;
}

对等线程的赋值语句和主线程的Accept语句间引入的竞争:主线程Accept之后,创建新线程执行thread线程例程,同时主线程继续Accept,如果主线程的Accept在新线程的赋值语句之前执行,那么之前的连接就没有被处理,而是处理的下一次连接。因此必须让每个Accept返回的已连接描述符分配到不同的动态存储器块。

编译:gcc -o echoservert echoservert.c csapp.c csapp.h -lpthread

多线程程序中的共享变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "csapp.h"
#define N 2
void *thread(void *vargp);
char **ptr;
int main()
{
int i;
pthread_t tid;
char *msgs[N] = {
"Hello from foo",
"Hello from bar"
};

ptr = msgs;
for (i = 0; i < N; i++)
Pthread_create(&tid, NULL, thread, (void *)i);
Pthread_exit(NULL);
}

void *thread(void *vargp)
{
int myid = (int)vargp;
static int cnt = 0;
printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt);
return NULL;
}

编译:gcc -o sharing sharing.c csapp.c csapp.h -lpthread

线程存储器模型

一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文,包括线程ID栈指针程序计数器条件码通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成的。
将变量映射到存储器

  • 全局变量 在运行时,虚拟存储器的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用。
  • 本地自动变量 每个线程的栈都包含它自己的所有本地自动变量的实例。
  • 本地静态变量 和全局变量一样,虚拟存储器的读/写区域只包含在程序中声明的每个本地静态变量的一个实例。

用信号量同步线程

一个共享变量引入同步错误(synchronization)的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "csapp.h"

void *thread(void *vargp);
volatile int cnt = 0;

int main(int argc, char **argv)
{
int niters;
pthread_t tid1, tid2;
if (argc != 2) {
printf("usage: %s <niters>\n", argv[0]);
}
niters = atoi(argv[1]);

Pthread_create(&tid1, NULL, thread, &niters);
Pthread_create(&tid2, NULL, thread, &niters);
Pthread_join(tid1, NULL);
Pthread_join(tid2, NULL);

if (cnt != (2 * niters))
printf("BOOM! cnt=%d\n", cnt);
else
printf("OK cnt=%d\n", cnt);
exit(0);
}

void *thread(void *vargp)
{
int i, niters =*((int *)vargp);
for (i = 0; i < niters; i++)
cnt++;
return NULL;
}

编译:gcc -o badcnt badcnt.c csapp.c csapp.h -lpthread
执行:

1
2
3
4
➜  pthread ./badcnt 10000000
BOOM! cnt=18624047
➜ pthread ./badcnt 10000000
BOOM! cnt=12824971

会发现当niters足够大时,得到的答案会是错误的,而且每次都不同。因为当badcnt.c中的两个对等线程在一个单处理器上并发运行时,机器指令以某种顺序一个接一个地完成。这些顺序中的一些将会产生正确结果,但其他的则不会。一般而言,没有办法预测操作系统是否将为你的线程选择一个正确的顺序。

信号量

信号量s是具有非负整数值的全局变量,只能由两种特殊的操作来处理:

  • P(s) 如果s是非零的,P将s减一,并立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。
  • V(s) V操作将s加一。如果有任何线程阻塞在P操作等待s变成非零,那么V操作会重启这些线程中的一个。

当有多个线程在等待同一个信号量时,不能预测V操作要重启哪个线程。
Posix标准定义了许多操作信号量的函数。

1
2
3
4
5
6
#include <semaphore.h>
// 将信号量sem初始化为value,每个信号量使用前必须初始化
int sem_init(sem_t *sem, 0, unsigned int value);
// 程序分别通过调用sem_wait和sem_post函数来执行P和V操作。
int sem_wait(sem_t *s);
int sem_post(sem_t *s);

可以用以下包装函数代替

1
2
3
#include "csapp.h"
void P(sem_t *s); // sem_wait的包装函数
void V(sem_t *s); // sem_post的包装函数

使用信号量来实现互斥

基本思想是将共享变量与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。

临界区:对于线程i,操作共享变量cnt内容的指令构成了一个临界区(critical section)。
要确保每个线程在执行它的临界区中的指令时,拥有对共享变量的互斥的访问,这种现象称为互斥(mutual exclusion)。

以这种方式保护共享变量的信号量叫做二元信号量(binary semaphore),因为它的值为0或1。以提供互斥为目的的二元信号量也成为互斥锁(mutex)。在一个互斥锁上执行P操作称为对互斥锁加锁,V操作称为解锁。对一个互斥锁加了锁但是还没有解锁的线程称为占用这个互斥锁。
一个被用作一组可用资源的计数器的信号量称为计数信号量
用信号量正确同步前面的计数器程序实例:
1.首先声明一个信号量mutex

1
2
volatile int cnt = 0;
sem_t mutex;

2.在主例程中,pthread_create之前将mutex初始化为1

1
Sem_init(&mutex, 0, 1);

3.通过在线程例程中对共享变量cnt的更新包围P和V操作

1
2
3
4
5
for (i = 0; i < niters; i++) {
P(&mutex);
cnt++;
V(&mutex);
}

再编译执行就一定能得到正确结果了

1
2
3
4
5
➜  pthread gcc -o badcnt badcnt.c csapp.c csapp.h -lpthread
➜ pthread ./badcnt 10000000
OK cnt=20000000
➜ pthread ./badcnt 100000000
OK cnt=200000000

利用信号量来调度共享资源

一个线程通过信号量操作来通知另一个线程,程序状态中某个条件已经为真了。两个经典而有用的例子是生产者 - 消费者读者 - 写者问题。
1.生产者 - 消费者问题
生产者和消费者线程共享一个有n个槽的有限缓冲区。生产者线程反复地生成新的项目,并把它们插入到缓冲区中。消费者线程不断地从缓冲区中取出这些项目,然后消费它们。
因为插入和取出项目都涉及更新共享变量,所以我们必须保证对缓冲区的访问是互斥的。但只保证互斥访问是不够的,还需要调度对缓冲区的访问。如果缓冲区是满的,生产者就必须等到有一个槽位变为可用。如果缓冲区是空的,那么消费者必须等到有一个可用项目。
下面开发一个简单的包,叫做SBUF,用来构造生产者 - 消费者程序。
SBUF操作类型为sbuf_t的有限缓冲区。

1
2
3
4
5
6
7
8
9
10
11
// sbuf.h
#include "csapp.h"
typedef struct {
int *buf; // 存放项目的动态分配的n项整数数组
int n; // 槽位的个数
int front; // 索引值,(front+1)%n记录数组第一项
int rear; // 索引值,rear%n记录数组最后一项
sem_t mutex; // 提供互斥缓冲区访问的信号量
sem_t slots; // 空槽位数
sem_t items; // 可用项目数
} sbuf_t;

SBUF函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
include "csapp.h"
#include "sbuf.h"

void sbuf_init(sbuf_t *sp, int n)
{ // 为缓冲区分配堆存储器
sp->buf = Calloc(n, sizeof(int));
sp->n = n;
sp->front = sp->rear = 0; // 表示空缓冲区
Sem_init(&sp->mutex, 0, 1);
Sem_init(&sp->slots, 0, n);
Sem_init(&sp->items, 0, 0);
}

void sbuf_deinit(sbuf_t *sp)
{ // 应用程序用完缓冲区时,释放缓冲区存储
Free(sp->buf);
}

void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); // wait for available slot
P(&sp->mutex); // lock the buffer
sp->buf[(++sp->rear) % (sp->n)] = item; // insert the item
V(&sp->mutex); // unlock the buffer
V(&sp->items); // announce available item
}

int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); // wait for available slot
P(&sp->mutex);
item = sp->buf[(++sp->front) % (sp->n)];
V(&sp->mutex);
V(&sp->slots);
return item;
}

2.读者 - 写者问题
一组并发的线程要访问一个共享对象,有些线程只读对象,而其他的线程只修改对象。修改对象的线程叫做写者,只读对象的线程叫做读者。写者必须拥有对对象的独占的访问,而读者可以和无限多个其他的读者共享对象。
第一类读者 - 写者问题 读者优先,要求不让读者等待,除非已经有写者在占用。
第二类读者 - 写者问题 写者优先,在写者之后到的读者要等待。
第一类读者 - 写者问题的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int readcnt; // 共享变量,统计当前在临界区中的读者数量
sem_t mutex, w; // mutex保护对readcnt的访问,w控制对访问共享对象的临界区的访问

void reader(void)
{
while (1) {
P(&mutex);
readcnt++;
if (readcnt == 1) // first in
P(&w);
V(&mutex);
/* Critical section
Reading happens */
P(&mutex);
readcnt--;
if (readcnt == 0) // last out
V(&w);
V(&mutex);
}
}

void writer(void)
{
while (1) {
P(&w);
/* Critical section
Writing happens */
V(&w);
}
}

综合:基于预线程化的并发服务器

一个基于预线程化(prethreading)的服务器通过使用生产者 - 消费者模型来降低为每一个新客户端创建一个新线程的开销。服务器是由一个主线程和一组工作者线程构成的。主线程不断地接收来自客户端的连接请求,并将得到的连接描述符放在一个有限缓冲区中。每一个工作者线程反复地从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。
用SBUF包实现一个预线程化的并发echo服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// echoservert_pre.c
#include "csapp.h"
#include "sbuf.h"
#define NTHREADS 4
#define SBUFSIZE 16

void echo_cnt(int connfd);
void *thread(void *vargp);

sbuf_t sbuf;

int main(int argc, char **argv)
{
int i, listenfd, connfd, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
pthread_t tid;

if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
sbuf_init(&sbuf, SBUFSIZE); // 初始化缓冲区sbuf
listenfd = Open_listenfd(port);

for (i = 0; i < NTHREADS; i++) { // 主线程创建了一组工作者线程
Pthread_create(&tid, NULL, thread, NULL);
}

while (1) { // 进入无限的服务器循环,接受连接请求
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
sbuf_insert(&sbuf, connfd); // 将得到的已连接的描述符插入到缓冲区sbuf中。
}
}

void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while (1) { // 每个工作者线程等待直到它能从缓冲区中取出一个已连接的描述符
int connfd = sbuf_remove(&sbuf);
echo_cnt(connfd);
Close(connfd);
}
}

echo_cnt函数的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// echo_cnt.c
include "csapp.h"
static int byte_cnt; // 记录了所有客户端接收到的累计字节数
static sem_t mutex;

static void init_echo_cnt(void)
{
Sem_init(&mutex, 0, 1);
byte_cnt = 0;
}

void echo_cnt(int connfd)
{
int n;
char buf[MAXLINE];
rio_t rio;
static pthread_once_t once = PTHREAD_ONCE_INIT;
Pthread_once(&once, init_echo_cnt); // 当第一次有某个线程调用echo_once函数时,使用pthread_once调用初始化函数
Rio_readinitb(&rio, connfd);
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
P(&mutex); // 对共享变量byte_cnt进行保护
byte_cnt += n;
printf("thread %d received %d (%d total) bytes on fd %d\n",
(int)pthread_self(), n, byte_cnt, connfd);
V(&mutex);
Rio_writen(connfd, buf, n);
}
}

编译:gcc -o echoservert_pre echoservert_pre.c sbuf.h sbuf.c csapp.h csapp.c echo_cnt.c -lpthread
reference
《深入理解计算机系统》

ptrace on Android

无论是hook还是调试都离不开ptrace这个system call,ptrace可以跟踪目标进程,并且在目标进程暂停的时候对目标进程的内存进行读写。
首先看一下要ptrace的目标程序,用来一直循环输出一句话”Hello, Hooking!”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int count = 0;
void targetFunc(int number)
{
char *str = "Hello, Hooking!";
printf("%s %d\n", str, number);
}

int main()
{
while (1) {
targetFunc(count);
count++;
sleep(1);
}
return 0;
}

要编译它需要先建立一个Android.mk文件,内容如下,让ndk将文件编译为elf可执行文件:

1
2
3
4
5
6
7
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := target
LOCAL_SRC_FILES := target.c

include $(BUILD_EXECUTABLE)

只有设置Android SDK<=9编译出的elf文件才是executable的,否则编译出的是shared object(即使是include的BUILD_EXECUTABLE)。

接下来写出hook1.c程序来hook target程序的system call,main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(int argc, char *argv[])
{
if (argc != 2) {
printf("Usage: %s <pid to be traced>\n", argv[0]);
return 1;
}
pid_t pid;
int status;
pid = atoi(argv[1]);

if (0 != ptrace(PTRACE_ATTACH, pid, NULL, NULL)) {
printf("Trace process failed:%d.\n", errno);
return 1;
}
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
while (1) {
wait(&status);
hookSysCallBefore(pid);
ptrace(PTARCE_SYSCALL, pid, NULL, NULL);

wait(&status);
hookSysCallAfter(pid);
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
}
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}

首先要知道hook目标进程的pid,用ps命令获取。然后使用ptrace(PTRACE_ATTACH, pid, NULL, NULL)这个函数对目标进程进行加载。加载成功后,我们可以使用ptrace(PTRACE_SYSCALL, pid, NULL, NULL)这个函数来对目标程序下断点,每当目标程序调用system call前的时候,就会暂停下来。然后可以读取寄存器的值来获取system call的各项信息。再一次使用ptrace(PTRACE_SYSCALL, pid, NULL, NULL)这个函数就可以让system call在调用完成后再一次暂停下来,并获取system call的返回值。
获取system call编号的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
long getSysCallNo(int pid, struct pt_regs *regs)
{
long scno = 0;
scno = ptrace(PTRACE_PEEKTEXT, pid, (void *)(regs->ARM_pc - 4), NULL);
if (scno = 0) return 0;
if (scno == 0xef000000) {
scno = regs->ARM_r7;
} else {
if ((scno & 0x0ff00000) != 0x0f900000) {
return -1;
}
scno &= 0x000fffff;
}
return scno;
}

ARM架构上,所有的系统调用都是通过SWI来实现的。并且在ARM架构中有两个SWI指令,分别针对EABI和OABI:

[EABI]
机器码: 1110 1111 0000 0000 – SWI 0
具体的调用号存放在寄存器r7中。
[OABI]
机器码: 1101 1111 vvvv vvvv – SWI immed_8
调用号进行转换后得到指令中的立即数。立即数=调用号 | 0x900000

需要兼容两种方法的调用,在代码上就要分开处理。首先要获取SWI指令判断是EABI还是OABI,如果是EABI,可从r7中获取调用号。如果是OABI,则从SWI指令中获取立即数,反向计算出调用号。
接着看hook system call前的函数,和hook system call后的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void hookSysCallBefore(pid_t pid)
{
struct pt_regs regs;
int sysCallNo = 0;

ptrace(PTRACE_GETREGS, pid, NULL, &regs);
sysCallNo = getSysCallNo(pid, &regs);
printf("Before SysCallNo = %d\n", sysCallNo);

if (sysCallNo == __NR_write) {
printf("__NR_write: %ld %p %ld\n", regs.ARM_r0, (void*)regs.ARM_r1, regs.ARM_r2);
}
}

void hookSysCallAfter(pid_t pid)
{
struct pt_regs regs;
int sysCallNo = 0;

ptrace(PTRACE_GETREGS, pid, NULL, &regs);
sysCallNo = getSysCallNo(pid, &regs);

printf("After SysCallNo = %d\n", sysCallNo);

if (sysCallNo == __NR_write) {
printf("__NR_write return: %ld\n", regs.ARM_r0);
}
printf("\n");
}

在获取了system call的调用号后,可以进一步获取各个参数的值,比如说wirte这个system call有三个参数。在arm上,如果形参个数少于或等于4,则形参由R0R1R2R3四个寄存器传递。大于四个则通过栈传递。函数的返回值保存在R0中。
把target和hook1 push到/data/local/tmp目录下,再chmod 777,接着运行target:

1
2
3
4
5
6
root@hammerhead:/data/local/tmp # ./target
Hello, Hooking! 0
Hello, Hooking! 1
Hello, Hooking! 2
Hello, Hooking! 3
...

再开一个shell,ps获取target的pid,使用hook1程序对target进行hook操作:

1
2
3
4
5
6
7
8
9
10
11
12
root@hammerhead:/data/local/tmp # ./hook1 27190
Before SysCallNo = 0
After SysCallNo = 0

Before SysCallNo = 4
__NR_write: 1 0x4f5020 20
After SysCallNo = 4
__NR_write return: 20

Before SysCallNo = 162
After SysCallNo = 162
...

syscall No 162是sleep函数,syscall No 4是write函数,因为printf本质就是调用write这个系统调用。对write函数参数的解析:1是stdout即标准输出,0x4f5020是字符串地址,20表示字符串长度。返回值20是write成功写入的长度。
整个过程的图示:

利用ptrace动态修改内存

下面演示用ptrace进行内存读写,将write()输出的string进行翻转。
在hook1.c的基础上继续进行修改,在hookSysCallBefore()函数中加入modifyString(pid, regs.ARM_r1, regs.ARM_r2)这个函数:

1
2
3
4
if (sysCallNo == __NR_write) {
printf("__NR_write: %ld %p %ld\n", regs.ARM_r0, (void *)regs.ARM_r1, regs.ARM_r2);
modifyString(pid, regs.ARM_r1, regs.ARM_r2);
}

把write的第二个参数字符串地址r1和第三个参数字符串长度r2传递给modifyString()这个函数:

1
2
3
4
5
6
7
8
void modifyString(pid_t pid, long addr, long strlen)
{
char *str;
str = (char *)calloc((strlen+1) * sizeof(char), 1);
getdata(pid, addr, str, strlen);
reverse(str);
putdata(pid, addr, str, strlen);
}

modifyString()首先获取在内存中的字符串,然后进行翻转操作,最后把翻转后的字符串写入原来的地址。这些操作用到了getdata()和putdata()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void getdata(pid_t child, long addr, char *str, int len)
{
char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
} data;
i = 0;
j = len / long_size;
laddr = str;
while (i < j) {
data.val = ptrace(PTRACE_PEEKDATA, child, addr+i*4, NULL); // ptrace的内存操作一次只能控制4个字节
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if (j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child, addr+i*4, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}

void putdata(pid_t child, long addr, char *str, int len)
{
char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
} data;
i = 0;
j = len / long_size;
laddr = str;
while (i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child, addr+i*4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if (j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, addr+i*4, data.val);
}
}

getdata()putdata()分别使用PTRACE_PEEKDATAPTRACE_POKEDATA对内存进行读写操作。因为ptrace的内存操作一次只能控制4个字节,所以如果修改比较长的内容需要进行多次操作。
现在运行target,并且在运行中用hook2进行hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@hammerhead:/data/local/tmp # ./target
Hello, Hooking! 0
Hello, Hooking! 1
Hello, Hooking! 2
Hello, Hooking! 3
Hello, Hooking! 4
Hello, Hooking! 5
Hello, Hooking! 6
Hello, Hooking! 7
Hello, Hooking! 8
Hello, Hooking! 9
01 !gnikooH ,olleH
11 !gnikooH ,olleH
21 !gnikooH ,olleH
31 !gnikooH ,olleH
41 !gnikooH ,olleH
51 !gnikooH ,olleH
Hello, Hooking! 16
Hello, Hooking! 17
Hello, Hooking! 18
Hello, Hooking! 19
....

运行hook2后字符串被翻转,退出hook2字符串回到原顺序。

利用ptrace动态执行sleep()函数

下面利用ptrace来执行libc.so中的sleep()函数,主要逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void inject(pid_t pid)
{
struct pt_regs old_regs, regs;
long sleep_addr;
// save old regs
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(&regs, &old_regs, sizeof(regs));

printf("getting remote sleep_addr:\n");
sleep_addr = get_remote_addr(pid, libc_path, (void *)sleep);

long parameters[1];
parameters[0] = 10;
ptrace_call(pid, sleep_addr, parameters, 1, &regs);
// restore old regs
ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
}

首先我们用ptrace(PTRACE_GETREGS, pid, NULL, &old_regs)获取当前寄存器的值,以便最后恢复数据。然后获取sleep()函数在目标进程中的地址,接着利用ptrace执行sleep()函数。
下面是获取sleep()函数在目标进程中地址的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void *get_module_base(pid_t pid, const char *module_name)
{
FILE *fp;
long addr = 0;
char *pch;
char filename[32], line[1024];
if (pid == 0) {
snprintf(filename, sizeof(filename), "/proc/self/maps");
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}
fp = fopen(filename, "r");
if (fp != NULL) {
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, module_name)) {
pch = strtok(line, "-");
addr = strtoul(pch, NULL, 16);
if (addr == 0x8000) // 如果被加载的文件是executable而不是so,则不需要加上基址
addr = 0;
break;
}
}
fclose(fp);
}
return (void*)addr;
}

long get_remote_addr(pid_t target_pid, const char *module_name, void *local_addr)
{
void *local_handle, *remote_handle;
local_handle = get_module_base(0, module_name);
remote_handle = get_module_base(target_pid, module_name);

printf("module_base: local[%p], remote[%p]\n", local_handle, remote_handle);
// 本进程函数地址减去本进程libc地址等于该函数在libc的偏移,再加上负载进程的libc基址即负载进程中函数地址
long ret_addr = (long)((uint32_t)local_addr - (uint32_t)local_handle + (uint32_t)remote_handle);

printf("remote_addr: [%p]\n", (void *)ret_addr);
return ret_addr;
}

因为libc.so在内存中的地址是随机的,所以要先获取目标进程的libc.so的加载地址,再获取本进程的libc.so的加载地址和sleep()在内存中的地址。然后我们就能计算出sleep()函数在目标进程中的地址了。要注意的是获取目标进程和本进程的libc.so的加载地址是通过解析/proc/[pid]/maps得到的。
接下来执行sleep()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int ptrace_call(pid_t pid, long addr, long *params, uint32_t num_params, struct pt_regs *regs)
{
uint32_t i;
// 将参数赋给R0-R3
for (i = 0; i < num_params && i < 4; ++i) {
regs->uregs[i] = params[i];
}
// 参数大于四个,将参数放在栈上
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * long_size;
putdata(pid, (long)regs->ARM_sp, (char *)&params[i], (num_params - i) * long_size);
}

regs->ARM_pc = addr;
if (regs->ARM_pc & 1) {
/* thumb */
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else {
/* arm */
regs->ARM_cpsr &= ~CPSR_T_MASK;
}

regs->ARM_lr = 0;
if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}

首先是将参数赋值给R0-R3,如果参数大于四个的话,再使用putdata()将参数存放在栈上。然后我们将PC的值设置为函数地址。接着再根据是否是thumb指令设置ARM_cpsr寄存器的值。随后我们使用ptrace_setregs()将目标进程寄存器的值进行修改。最后使用waitpid()等待函数被执行。

利用ptrace动态加载so并执行自定义函数

逻辑如下:

1
2
3
4
5
6
7
8
保存当前寄存器的状态
获取目标程序的mmap, dlopen, dlsym, dlclose地址
调用mmap分配一段内存空间来保存参数信息
调用dlopen加载so文件
调用dlsym找到目标函数地址
使用ptrace_call执行目标函数
调用dlclose卸载so文件
恢复寄存器的状态

实现整个逻辑的函数injectSo()的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void injectSo(pid_t pid, char *so_path, char *function_name, char *parameter)
{
struct pt_regs old_regs, regs;
long mmap_addr, dlopen_addr, dlsym_addr, dlclose_addr;

// save old regs
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(&regs, &old_regs, sizeof(regs));
// get remote address
printf("getting remote address:\n");
mmap_addr = get_remote_addr(pid, libc_path, (void *)mmap);
dlopen_addr = get_remote_addr(pid, libc_path, (void *)dlopen);
dlsym_addr = get_remote_addr(pid, libc_path, (void *)dlsym);
dlclose_addr = get_remote_addr(pid, libc_path, (void *)dlclose);

printf("mmap_addr = %p dlopen_addr=%p dlsym_addr=%p dlclose_addr=%p\n",
(void *)mmap_addr, (void *)dlopen_addr, (void *)dlsym_addr, (void *)dlclose_addr);
long parameters[10];

// mmap
parameters[0] = 0; // address
parameters[1] = 0x4000; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // WRX
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flag
parameters[4] = 0; // fd
parameters[5] = 0; // offset

ptrace_call(pid, mmap_addr, parameters, 6, &regs);
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
long map_base = regs.ARM_r0; // 返回映射区的指针

printf("map_base = %p\n", (void *)map_base);
// dlopen
printf("save os_path = %s to map_base %p\n", so_path, (void *)map_base);
putdata(pid, map_base, so_path, strlen(so_path)+1);

parameters[0] = map_base;
parameters[1] = RTLD_NOW | RTLD_GLOBAL;
ptrace_call(pid, dlopen_addr, parameters, 2, &regs);
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
long handle = regs.ARM_r0;

printf("handle = %p\n", (void *)handle);
// dlsym
printf("save function_name = %s to map_base = %p\n", function_name, (void *)map_base);
putdata(pid, map_base, function_name, strlen(function_name) + 1);

parameters[0] = handle;
parameters[1] = map_base;
ptrace_call(pid, dlsym_addr, parameters, 2, &regs);

ptrace(PTRACE_GETREGS, pid, NULL, &regs);
long function_ptr = regs.ARM_r0;

printf("function_ptr = %p\n", (void *)function_ptr);
// function_call
printf("save parameter = %s to map_base = %p\n", parameter, (void *)map_base);
putdata(pid, map_base, parameter, strlen(parameter)+1); //此处的parameter是通过参数传递进来的

parameters[0] = map_base;

ptrace_call(pid, function_ptr, parameters, 1, &regs);
// dlclose
parameters[0] = handle;
ptrace_call(pid, dlclose_addr, parameters, 1, &regs);
// restore old regs
ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
}

mmap()可以用来将一个文件或者其它对象映射进内存,如果我们把flag设置为MAP_ANONYMOUS并且把参数fd设置为0的话就相当于直接映射一段内容为空的内存。mmap()的函数声明和参数如下:

1
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。这里设置为RWX。
flags:指定映射对象的类型,映射选项和映射页是否可以共享。我们这里设置为:MAP_ANONYMOUS(匿名映射,映射区不与任何文件关联),MAP_PRIVATE(建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件)。
fd:有效的文件描述词。匿名映射设置为0。
offset:被映射对象内容的起点。设置为0。

mmap()映射的内存主要用来保存传递给其他函数的参数。比如接下来我们需要用dlopen()去加载”/data/local/tmp/libinject.so”这个文件,所以需要先用putdata()将字符串”/data/local/tmp/libinject.so”放置在mmap()所映射的内存中,再将映射地址作为参数传递给dlopen()。接下来的dlsym(),so中的目标函数,dlclose()都是相同的调用方式。
被加载的so文件内容如下:

1
2
3
4
5
6
7
8
int injectedFunc(char *str)
{
printf("injected function pid = %d\n", getpid());
printf("Hello %s\n", str);
LOGD("injected function pid = %d\n", getpid());
LOGD("Hello %s\n", str);
return 0;
}

这里不光使用printf()还使用了android debug的函数LOGD()用来输出调试结果。所以在编译时我们需要加上LOCAL_LDLIBS := -llog
编译完后使用hook4对target进行注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
root@hammerhead:/data/local/tmp # ./target
Hello, Hooking! 0
Hello, Hooking! 1
Hello, Hooking! 2
Hello, Hooking! 3
injected function pid = 13574
Hello Android hooking
Hello, Hooking! 4
Hello, Hooking! 5
Hello, Hooking! 6
...

root@hammerhead:/data/local/tmp # ./hook4 13574
getting remote address:
module_base: local[0xb6f7c000], remote[0xb6ec0000]
remote_addr: [0xb6ed2c5d]
module_base: local[0xb6f7c000], remote[0xb6ec0000]
remote_addr: [0xb6f22f31]
module_base: local[0xb6f7c000], remote[0xb6ec0000]
remote_addr: [0xb6f22e81]
module_base: local[0xb6f7c000], remote[0xb6ec0000]
remote_addr: [0xb6f22dfd]
mmap_addr = 0xb6ed2c5d dlopen_addr=0xb6f22f31 dlsym_addr=0xb6f22e81 dlclose_addr=0xb6f22dfd
map_base = 0xb6e82000
save os_path = /data/local/tmp/libinject.so to map_base 0xb6e82000
handle = 0xb6f1f494
save function_name = injectedFunc to map_base = 0xb6e82000
function_ptr = 0xb6e7cc61
save parameter = Android hooking to map_base = 0xb6e82000

可以看到stdout和logcat都成功输出了调试信息。这意味着可以通过注入让目标进程加载so文件并执行任意代码了。

利用函数挂钩实现native层的hook

这一节要实现用函数挂钩hook目标函数,函数挂钩的基本原理是先用mprotect()将原代码段改成可读可写可执行,然后修改原函数的入口处的代码,让pc指针跳转到动态加载的so文件中的hook函数中,执行完hook函数以后再让pc指针跳转回原本的函数中。
用来注入的程序hook5逻辑与hook4相比并没有太大变化,仅仅少了”调用dlclose卸载so文件”这一个步骤,因为要执行的hook后的函数在so中,所以不需要卸载,步骤如下:

1
2
3
4
5
6
7
保存当前寄存器的状态
获取目标程序的mmap, dlopen, dlsym地址
调用mmap分配一段内存空间用来保存参数信息
调用dlopen加载so文件
调用dlsym找到目标函数地址
使用ptrace_call执行目标函数
恢复寄存器的状态

hook5的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void injectSo(pid_t pid,char *so_path, char *function_name,char *parameter)
{
struct pt_regs old_regs,regs;
long mmap_addr, dlopen_addr, dlsym_addr;

// save old regs
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(&regs, &old_regs, sizeof(regs));

// get remote address
printf("getting remote addres:\n");
mmap_addr = get_remote_addr(pid, libc_path, (void *)mmap);
dlopen_addr = get_remote_addr(pid, libc_path, (void *)dlopen);
dlsym_addr = get_remote_addr(pid, libc_path, (void *)dlsym);

printf("mmap_addr=%p dlopen_addr=%p dlsym_addr=%p\n",
(void*)mmap_addr,(void*)dlopen_addr,(void*)dlsym_addr);

long parameters[10];

// mmap
parameters[0] = 0; // address
parameters[1] = 0x4000; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // WRX
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flag
parameters[4] = 0; // fd
parameters[5] = 0; // offset

ptrace_call(pid, mmap_addr, parameters, 6, &regs);
ptrace(PTRACE_GETREGS, pid, NULL, &regs);

long map_base = regs.ARM_r0;
printf("map_base = %p\n", (void*)map_base);

// dlopen
printf("save so_path = %s to map_base = %p\n", so_path, (void*)map_base);
putdata(pid, map_base, so_path, strlen(so_path) + 1);

parameters[0] = map_base;
parameters[1] = RTLD_NOW| RTLD_GLOBAL;

ptrace_call(pid, dlopen_addr, parameters, 2, &regs);
ptrace(PTRACE_GETREGS, pid, NULL, &regs);

long handle = regs.ARM_r0;

printf("handle = %p\n",(void*) handle);

// dlsym
printf("save function_name = %s to map_base = %p\n", function_name, (void*)map_base);
putdata(pid, map_base, function_name, strlen(function_name) + 1);

parameters[0] = handle;
parameters[1] = map_base;

ptrace_call(pid, dlsym_addr, parameters, 2, &regs);
ptrace(PTRACE_GETREGS, pid, NULL, &regs);

long function_ptr = regs.ARM_r0;

printf("function_ptr = %p\n", (void*)function_ptr);

// function_call
printf("save parameter = %s to map_base = %p\n", parameter, (void*)map_base);
putdata(pid, map_base, parameter, strlen(parameter) + 1);

parameters[0] = map_base;

ptrace_call(pid, function_ptr, parameters, 1, &regs);

// restore old regs
ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
}

arm处理器支持两种指令集,一种是arm指令集,另一种是thumb指令集。所以要hook的函数可能是被编译成arm指令集的,也有可能是被编译成thumb指令集的。需要注意的是thumb指令的长度是不固定的,但arm指令是固定的32位长度。
为了更容易地理解hook的原理,先只考虑arm指令集,因为arm相比thumb要简单一点,不需要考虑指令长度的问题。所以我们需要将target和hook的so编译成arm指令集的形式。很简单,只要在Android.mk中的文件名后面加上”.arm”即可 (真正的文件不用加)。

1
2
3
4
5
6
7
8
9
10
include $(CLEAR_VARS)
LOCAL_MODULE := target
LOCAL_SRC_FILES := target.c.arm
include $(BUILD_EXECUTABLE)

include $(CLEAR_VARS)
LOCAL_MODULE := inject2
LOCAL_SRC_FILES := inject2.c.arm
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

确定了指令集以后,来看实现挂钩最重要的逻辑,这个逻辑是在注入的so里实现的。首先我们需要一个结构体保存汇编代码和hook地址:

1
2
3
4
5
6
struct hook_t {
unsigned int jump[3]; // 保存跳转指令
unsigned int store[3]; // 保存原指令
unsigned int orig; // 保存原函数地址
unsigned int patch; // 保存hook函数地址
};

接着来看注入的逻辑,最重要的函数为hook_direct(),他有三个参数,1)最开始定义的用来保存汇编代码和hook地址的结构体,2)要hook的原函数的地址,3)用来执行的hook函数地址。函数的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int hook_direct(struct hook_t *h, unsigned int addr, void *hookf)
{
int i;

printf("addr = %x\n", addr);
printf("hookf = %x\n", (unsigned int)hookf);

// 将代码段改成可读可写可执行
mprotect((void*)0x8000, 0xa000-0x8000, PROT_READ|PROT_WRITE|PROT_EXEC);

// modify function entry
h->patch = (unsigned int)hookf; // hook函数地址
h->orig = addr; // 原函数地址
h->jump[0] = 0xe59ff000; // 把目标函数第一条指令改成 LDR pc, [pc, #0];跳转到PC指针所指的地址
h->jump[1] = h->patch; // 由于pc寄存器读出的值实际上是当前指令地址加8,所以我们把后面两处指令
h->jump[2] = h->patch; // 都保存为hook函数的地址,这样的话,我们就能控制PC跳转到hook函数的地址了。
for (i = 0; i < 3; i++) // 保存原函数的前三条指令
h->store[i] = ((int*)h->orig)[i];
for (i = 0; i < 3; i++) // 将函数入口指令改成跳转指令
((int*)h->orig)[i] = h->jump[i];

// 刷新指令的缓存。因为虽然前面的操作修改了内存中的指令,但有可能被修改的指令已经被缓存起来了
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig + sizeof(h->jump));
return 1;
}

虽然android有ASLR,但并没有PIE,所以program image是固定在0x8000这个地址的,因此我们用mprotect()函数将整个target代码段变成RWX,这样我们就能修改函数入口处的代码了。是否修改成功可以通过cat /proc/[pid]/maps查看:

1
2
3
4
root@hammerhead:/ # cat /proc/18029/maps
00008000-0000a000 rwxp 00000000 b3:1c 671749 /data/local/tmp/target
0000a000-0000b000 r--p 00001000 b3:1c 671749 /data/local/tmp/target
...

然后需要确定目标函数的地址,这个有两种方法。1)如果目标程序本身没有被strip的话,那些symbol都是存在的,因此可以使用dlopen()和dlsym()等方法来获取目标函数地址。但很多情况,目标程序都会被strip,特别是可以直接运行的二进制文件默认都会被直接strip。比如target中的targetFunc()这个函数名会在编译的时候去掉,所以使用dlsym()的话是无法找到这个函数的。2)这时候我们就需要使用IDA或者objdump来定位一下目标函数的地址。比如用IDA找一下target程序里面targetFunc(int number)这个函数的地址:

虽然target这个binary被strip了,但还是可以找到targetFunc()这个函数的起始地址是在0x84c4。一般ARM程序在IDA中打开后,自定义的函数都在Functions window的前几个:

最后一个参数也就是我们要执行的hook函数的地址。得到这个地址非常简单,因为是so中的函数,调用hook_direct()的时候直接写上函数名即可。

1
hook_direct(&eph, hookaddr, hookFunc);

hook_cacheflush()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void inline hook_cacheflush(unsigned int begin, unsigned int end)
{
const int syscall = 0xf0002;

__asm __volatile (
"mov r0, %0\n"
"mov r1, %1\n"
"mov r7, %2\n"
"mov r2, #0x0\n"
"svc 0x00000000\n"
:
: "r" (begin), "r" (end), "r" (syscall)
: "r0", "r1", "r7"
);
}

刷新完缓存后,再执行到原函数的时候,pc指针就会跳转到自定义的hook函数中了,hook函数里的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void  __attribute__ ((noinline)) hookFunc(int number)
{
printf("targetFunc() called, number = %d\n", number);
number *= 2;

void (*orig_targetFunc)(int number);
orig_targetFunc = (void *)eph.orig;

hook_precall(&eph);
orig_targetFunc(number);
hook_postcall(&eph);
}

首先在hook函数中,可以获得原函数的参数(参数已经在寄存器中了,编写hook函数的时候,参数与原函数相同即可),并且可以对原函数的参数进行修改,比如说将数字乘2。随后使用hook_precall(&eph);将原本函数的内容进行还原。hook_precall()内容如下:

1
2
3
4
5
6
7
8
void hook_precall(struct hook_t *h)
{
int i;
for (i = 0; i < 3; i++)
((int*)h->orig)[i] = h->store[i];

hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jump)*10);
}

在hook_precall()中,先对原本的三条指令进行还原,然后使用hook_cacheflush()对内存进行刷新。经过处理之后,就可以执行原来的函数orig_targetFunc(number)了。执行完后,如果还想再次hook这个函数,就需要调用hook_postcall(&eph)将原本的三条指令再进行一次修改。
下面用hook5和libinject2.so来注入以下target这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@hammerhead:/data/local/tmp # ./target
Hello, Hooking! 0
Hello, Hooking! 1
Hello, Hooking! 2
Hello, Hooking! 3
Hello, Hooking! 4
Hello, Hooking! 5
Hello, Hooking! 6
Hook Function pid = 18561
Hello HookFunction
addr = 84c4
hookf = b6ea3da5
targetFunc() called, number = 7
Hello, Hooking! 14
targetFunc() called, number = 8
Hello, Hooking! 16
targetFunc() called, number = 9
Hello, Hooking! 18
targetFunc() called, number = 10
Hello, Hooking! 20
targetFunc() called, number = 11
Hello, Hooking! 22
...

root@hammerhead:/data/local/tmp # ./hook5 18561
getting remote addres:
mmap_addr=0xb6ef9c5d dlopen_addr=0xb6f49f31 dlsym_addr=0xb6f49e81
map_base = 0xb6ea9000
save so_path = /data/local/tmp/libinject2.so to map_base = 0xb6ea9000
handle = 0xb6f46494
save function_name = hookEntry to map_base = 0xb6ea9000
function_ptr = 0xb6ea3e3d
save parameter = HookFunction to map_base = 0xb6ea9000

reference
http://drops.wooyun.org/tips/9300
http://drops.wooyun.org/papers/10156

radare2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ radare2 -h # 查看帮助
$ r2 -d file # 调试一个文件
> d? #显示调试命令
|Usage: d # Debug commands
| db[?] Breakpoints commands
| dc[?] Continue execution
| dm[?] Show memory maps
| dr[?] Cpu registers
...
> v # 进入visual mode,q退出;
# c可以显示一个游标;
# 按shift加hjkl可以选中;
# visual mode中可以用i来overwrite字节;
# p/P切换其他visual mode view;
# s - step into,S - step over当前指令;
# b下断点;
# : 可以在visual mode中输入常规的radare命令

Seeking

1
2
3
4
5
6
7
8
9
> s?
> s 0x0804848c # 查找这个地址,可以当跳转用
> s- # 撤销查找
> s+ # 重做查找
> s* # 列出撤销查找的历史
> s/ DATA # 查找下一处'DATA'
> s/x 85ff # 查找下一处\x85\xff
> sf # 查找下一个函数
> sr eip # 查找寄存器

Block Size

1
2
3
4
> b?
> b # 显示当前block大小
> b+3 # 当前block大小加3
> b 0x100 # 把block大小设置为0x100

Sections

1
2
3
4
> S?
> S # 列出sections
> S. # 展示当前section名
...

Flags
类似于书签

1
2
3
4
5
> f flag_name @ offset # create a flag type
> f- flag_name # 删除一个flag
> fs # 切换flagspace或创建新的flagspace
> fs symbols # 只选中symbols里的flag
> f # 列出flagspace里的flag

rabin2

File type identification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ rabin2 -I test
havecode true
pic false
canary false
nx true
crypto false
va true
intrp /lib64/ld-linux-x86-64.so.2
bintype elf
class ELF64
lang c
arch x86
bits 64
...

Code Entrypoints

1
2
3
4
5
$ rabin2 -e test
[Entrypoints]
vaddr=0x004004b0 paddr=0x000004b0 baddr=0x00400000 laddr=0x00000000 type=program

1 entrypoints

Imports&PLT

1
2
3
4
5
6
7
8
$ rabin2 -i test | head
[Imports]
ordinal=001 plt=0x00400470 bind=GLOBAL type=FUNC name=printf
ordinal=002 plt=0x00400480 bind=GLOBAL type=FUNC name=__libc_start_main
ordinal=003 plt=0x00400490 bind=UNKNOWN type=NOTYPE name=__gmon_start__
ordinal=004 plt=0x004004a0 bind=GLOBAL type=FUNC name=__isoc99_scanf

4 imports

Symbols(Exports)

1
2
3
4
5
6
7
rabin2 -s test | head
[Symbols]
vaddr=0x00600e20 paddr=0x00000e20 ord=028 fwd=NONE sz=0 bind=LOCAL type=OBJECT name=__JCR_LIST__
vaddr=0x004004e0 paddr=0x000004e0 ord=029 fwd=NONE sz=0 bind=LOCAL type=FUNC name=deregister_tm_clones
vaddr=0x00400510 paddr=0x00000510 ord=030 fwd=NONE sz=0 bind=LOCAL type=FUNC name=register_tm_clones
vaddr=0x00400550 paddr=0x00000550 ord=031 fwd=NONE sz=0 bind=LOCAL type=FUNC name=__do_global_dtors_aux
...

List Libraries

1
2
3
4
5
rabin2 -l test
[Linked libraries]
libc.so.6

1 library

Strings

1
rabin2 -z test | head

Program Sections

1
2
3
4
5
6
7
8
9
10
11
12
13
$ rabin2 -S test
rabin2 -S test
[Sections]
idx=00 vaddr=0x00000000 paddr=0x00000000 sz=0 vsz=0 perm=----- name=
idx=01 vaddr=0x00400238 paddr=0x00000238 sz=28 vsz=28 perm=--r-- name=.interp
idx=02 vaddr=0x00400254 paddr=0x00000254 sz=32 vsz=32 perm=--r-- name=.note.ABI_tag
idx=03 vaddr=0x00400274 paddr=0x00000274 sz=36 vsz=36 perm=--r-- name=.note.gnu.build_id
idx=04 vaddr=0x00400298 paddr=0x00000298 sz=28 vsz=28 perm=--r-- name=.gnu.hash
idx=05 vaddr=0x004002b8 paddr=0x000002b8 sz=120 vsz=120 perm=--r-- name=.dynsym
idx=06 vaddr=0x00400330 paddr=0x00000330 sz=88 vsz=88 perm=--r-- name=.dynstr
...

40 sections

rasm2

1
2
3
4
5
6
$ rasm2 -a java 'nop'
00
$ rasm2 -a x86 -d '90'
nop
$ rasm2 -a x86 -b 64 'syscall'
0f05

rahash2

1
2
$ rahash2 file -a md5
file: 0x00000000-0x00000072 md5: b42ebe5fad4e8f020c8153a5b748ad2b

radiff2

无参数运行radiff2显示修改的字节及对应的偏移

1
2
3
4
$ radiff2 test test-c
radiff2 test test-c
Buffer truncated to 8610 bytes (1 not compared)
0x00000200 52 => 02 0x00000200

radiff2可以比较两个文件的相似度和距离

1
2
3
4
$ radiff2 -s test test-c
Processing 8610 of 8609
similarity: 1.000
distance: 2

rafind2

ragg2

rarun2

useful for:

  • Crackmes
  • Fuzzing
  • Test suites

rax2

1
2
3
4
5
6
7
8
9
10
$ rax2 1337
0x539
$ rax2 -b 01111001
y
$ rax2 -S AB
4142
$ rax2 -s 4142
AB
$ rax2 -e 33 # swap endianness
0x21000000

先写这些吧,后面看心情补充。。。

Android代码调试

与Java一样,Dalvik实现了一个标准的调试接口,称为Java调试线协议(Java Debug Wire Protocol, JDWP)。所有用来调试Dalvik和Java上的程序的工具都是基于此协议开发的。 深入Java调试体系
Android设备监视器(Monitor)和Dalvik调试监视服务器(DDMS)都采用了JDWP标准协议,它们用JDWP访问指定应用的信息(线程、堆使用情况、正在进行的方法调用)
1.调试应用程序
点击工具栏中的Debug As图标(like a bug)进入Debug界面,想要返回代码界面点击右上角的Java按钮。

左上角的小窗是各个栈帧,点击某个栈帧会在代码窗口显示附近代码。
2.显示framework层源代码
点击栈帧时可以显示Android framework层源代码:首先要正确初始化AOSP资料库。文档
下一步,为Eclipse创建类路径。在AOSP根目录下运行make idegen命令创建idegen.sh脚本,在顶层目录下创建excluded-paths文件,以排除顶层目录下不想包含的所有目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
^abi/.*
^external/.*
^packages/.*
^cts/.*
^art/.*
^dalvik/.*
^development/.*
^prebuilts/.*
^out/.*
^tools/.*
^sdk/.*
^libcore/.*
^gdk/.*
^hardware/.*
^device/.*
^kernel/*
^pdk/*
^developers/*

新建一个Java Project命名为AOSP Framework Source,取消Use Default Location复选框,指定AOSP根目录。
调试刚才的示例应用,右键栈帧窗口中的项目名,选择Edit Source Lookup...Add路径为Java Project,选上一步创建的AOSP Framework Source项目,再点击父栈帧时会显示framework层的代码。

3.伪造调试设备
对于原厂设备,启动Android SDK中自带的DDMS或Monitor只显示可调试进程。

使用eng配置生成的工程设备允许访问所有进程。eng与user或userdebug之间的主要区别是系统属性ro.secure和ro.debuggable,user和userdebug生成时将这两个值设置为1和0;而eng生成时为0和1。
修改已root设备以支持调试系统服务和预装应用并不复杂,介绍一种简单但不是永久有效的方法,设备重启后失效。首先,获取一份setpropex工具,此工具可以在已root设备上修改只读的系统属性:

1
2
3
4
5
6
7
shell@hammerhead:/data/local/tmp $ su
root@hammerhead:/data/local/tmp # ./setpropex ro.secure 0
root@hammerhead:/data/local/tmp # ./setpropex ro.debuggable 1
root@hammerhead:/data/local/tmp # getprop ro.secure
0
root@hammerhead:/data/local/tmp # getprop ro.debuggable
1

断开shell连接,在主机上使用adb root命令以root权限重启ADB守护进程。
最后重启所有依赖Dalvik VM的进程。在修改ro.debuggable属性后启动的任何进程都是可调试的。为了强制重启Android Dalvik层,可以简单地结束system_server进程:

1
2
root@hammerhead:/data/local/tmp # ps
root@hammerhead:/data/local/tmp # kill -9 system_server_pid

设备重启后,Monitor中会出现所有的Dalvik进程。
4.附加到其他进程
处于完全调试模式下的设备也支持实时调试任何Dalvik进程。在Eclipse启动并处于运行状态下,点击右上角的DDMS,在Devices窗口中选择目标进程,比如system_process。在Run菜单中选择Debug Configurations打开对话框,在对话框左边双击Remote Java Application新建一个链接,Name设为Attacher,Connect选项卡中Project设为AOSP Framework Source项目,Host设为127.0.0.1,Port设为8700
最后点击Apply,点击Debug。

调试原生代码

关于如何使用原生代码编程可以参考《Android C++高级编程》
1.使用Eclipse进行调试
打开要调试的目标项目,首先,需要告知Android生成的应用必须支持调试:选择Project->Properties,点开C/C++生成选项并选择Environment,点击Add按钮,变量名输入NDK_DEBUG,值输入1。点击OK就可以开始调试了。为确保新的环境变量生效,选择Project->Build All
先在Java代码调用Native代码之前下断点,点击Debug As开启调试,再在Native代码中想要调试的位置下断点进行调试:

2.使用AOSP进行调试
编译AOSP代码,烧录到Nexus5设备上,具体过程可以参考我的另一篇文章
将GDB服务器二进制文件上传到设备:

1
2
android4.4.2 $ adb push prebuilts/misc/android-arm/gdbserver/gdbserver /data/local/tmp
android4.4.2 $ adb shell chmod 755 /data/local/tmp/gdbserver

调试过程使用标准的TCP/IP连接将GDB客户端连接到GDB服务器上。建议通过USB使用ADB进行调试。用ADB的端口转发功能为GDB客户端打开一个管道:

1
android4.4.2 $ adb forward tcp:31337 tcp:31337

下一步将GDB服务器执行目标程序或附加至进程:

1
2
3
4
5
6
7
8
9
~ $ adb shell
# 启动应用,也可以手动点开,命令行显得更专(zhuang)业(bi)一点
root@hammerhead # am start -n com.bruce.jnitest/.MainActivity
root@hammerhead # ps
...
u0_a90 15078 28517 926056 41384 ffffffff 4009573c S com.bruce.jnitest
...
root@hammerhead # ./gdbserver --attach tcp:31337 15078
# 或执行目标程序: ./gdbserver tcp:31337 ./debugfile

打开另一个终端:

1
2
3
4
5
6
android4.4.2 $ cd prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/bin
bin $ ./arm-eabi-gdb -q
(gdb) target remote :31337
Remote debugging using :31337
0x4009573c in ?? ()
(gdb)

在另一个终端中adb连接到设备,查看so库加载的基址:

1
2
3
4
5
6
root@hammerhead # cat /proc/15078/maps
...
74f1c000-74f20000 r-xp 00000000 b3:1c 1638414 /data/app-lib/com.bruce.jnitest-1/libJniTest.so
74f20000-74f21000 r--p 00003000 b3:1c 1638414 /data/app-lib/com.bruce.jnitest-1/libJniTest.so
74f21000-74f22000 rw-p 00004000 b3:1c 1638414 /data/app-lib/com.bruce.jnitest-1/libJniTest.so
...

从中可以看到so库的基址是74f1c000,在IDA中找到想要调试的代码地址,加上基址得到实际内存中代码的地址:

切换到gdb客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) break *0x74f1d998
Breakpoint 1 at 0x74f1d998
(gdb) x/i 0x74f1d998
0x74f1d998: mov r3, #0
(gdb) c
Continuing.
# 触发应用调用so库中的native代码
Program received signal SIGILL, Illegal instruction.
0x74f1d99c in ?? ()
(gdb) disas 0x74f1d99c,+20
Dump of assembler code from 0x74f1d99c to 0x74f1d9b0:
=> 0x74f1d99c: str r3, [r11, #-48] ; 0x30
0x74f1d9a0: b 0x74f1d9d0
0x74f1d9a4: ldr r2, [r11, #-48] ; 0x30
0x74f1d9a8: mvn r3, #27
0x74f1d9ac: lsl r2, r2, #2
End of assembler dump.
(gdb)

剩下就可以使用GDB命令进行调试了。
3.使用IDA调试
GDB对thumb指令支持不好,调试thumb指令时最好还是用IDA。

  • 打开ddms
    打开ddms才能打开调试端口,才能用jdb
  • adb push android_server /data/local/tmp/
    android_server在IDA安装目录的dbgsvr目录中
    1
    2
    3
    4
    adb shell
    cd /data/local/tmp
    chmod 777 android_server
    ./android_server
  • adb forward tcp:23946 tcp:23946
  • adb shell am start -D -n com.bruce.jnitest
  • IDA attach到目标应用上
    Debugger->Attach->Remote ARMLinux/Android debugger

    选择com.bruce.jnitest,进程过多可以ctrl+F查找
  • suspend on library loading
    Debugger->Debugger options...

    选择suspend on libaray load/unload,然后按F9继续执行。
  • jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

    弹出的框都点cancel就行了,在右边的Modules窗口中找到要调试的so库双击。

    在新窗口中找到想要调试的函数,右键添加断点,继续执行程序。(在linker时会停多次,继续执行即可)

    下面触发应用调用库函数,控制流即停在断点处

    有时在一个函数里无法使用F5,这时在函数中按P,IDA会把这段代码作为函数分析,再按F5即可。

reference
《Android安全攻防权威指南》
http://drops.wooyun.org/tips/6840