android逆向分析之smali語法


一 、smali數據類型

1.Dalvik字節碼

Davlik字節碼中,寄存器都是32位的,能夠支持任何類型,64位類型(Long/Double)用2個連續的寄存器表示;

Dalvik字節碼有兩種類型:原始類型;引用類型(包括對象和數組)

 原始類型:

         v   void  只能用於返回值類型
          Z   boolean
          B   byte
          S   short
          C   char
          I    int
          J    long(64位)
          F   float
          D   double(64位)
對象類型:
Lpackage/name/ObjectName;       相當於java中的package.name.ObjectName;
     L:表示這是一個對象類型
     package/name:該對象所在的包
    ;:表示對象名稱的結束
 
2.數組的表示形式:
[I  :表示一個整形的一維數組,相當於java的int[];對於多維數組,只要增加[ 就行了,[[I = int[][];注:每一維最多255個;
對象數組的表示形式:[Ljava/lang/String表示一個String的對象數組;
 
3.方法的表示形式:
  Lpackage/name/ObjectName;->methodName(III)Z
          Lpackage/name/ObjectName  表示類型
          methodName   表示方法名
          III   表示參數(這里表示為3個整型參數)說明:方法的參數是一個接一個的,中間沒有隔開
 
4.字段的表示形式:
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;即表示:包名,字段名和字段類型
 
5.寄存器指定
有兩種方式指定一個方法中有多少寄存器是可用的:
.registers  指令指定了方法中寄存器的總數
.locals    指令表明了方法中非參寄存器的總數,出現在方法中的第一行
 
6.方法的表示
方法有直接方法和虛方法兩種,直接方法的聲明格式如下:
.method<訪問權限>[修飾關鍵字]<方法原型>
<.locals>
[.parameter]
[.prologue]
[.line]
<代碼體>
.end method
 
訪問權限有public、private等,修飾關鍵字有static、constructor等。方法原型描述了方法的名稱、參數與返回值。
.registers指定了方法中寄存器的總數
.locals指定了方法中非參寄存器的總數(局部變量的個數);
.parameter指定了方法的參數;
.prologue指定了代碼的開始處;
.line指定了該處指令在源代碼中的位置。
 
注意:構造函數的返回類型為V,名字為<init>。
 
7.方法的傳參:
當一個方法被調用的時候,方法的參數被置於最后N個寄存器中;
例如:一個方法有2個參數,5個寄存器(v0~v4),那么,參數將置於最后2個寄存器(v3和v4)。非靜態方法中的第一個參數總是調用該方法的對象。
說明:對於靜態方法除了沒有隱含的this參數外,其他都一樣
 
8.寄存器的命名方式:V命名、P命名
第一個寄存器就是方法中的第一個參數寄存器。比較:使用P命名是為了防止以后如果在方法中增加寄存器,需要對參數寄存器重新進行編號的缺點。特別說明一下:Long和Double類型是64位的,需要2個連續的寄存器
例如:對於非靜態方法LMyObject->myMethod(IJZ)V,有4個參數:  LMyObject(隱含的),int,long,boolean
需要5個寄存器(因為long占有2個連續的寄存器)來存儲參數:
     P0    this(非靜態方法中這個參數是隱含的)
     P1    I (int)
     P2,P3  J (long)
     P4    Z(bool)
 
如果是靜態的就是三個參數
     P0   I (int)
     P1,P2 J (long)
     P3    Z(bool)
 
二、成員變量
 # static fields             定義靜態變量的標記
# instance fields        定義實例變量的標記
# direct methods       定義靜態方法的標記
# virtual methods      定義非靜態方法的標記
  
三、控制條件

"if-eq vA, vB, :cond_**"   如果vA等於vB則跳轉到:cond_**
"if-ne vA, vB, :cond_**"   如果vA不等於vB則跳轉到:cond_**
"if-lt vA, vB, :cond_**"    如果vA小於vB則跳轉到:cond_**
"if-ge vA, vB, :cond_**"   如果vA大於等於vB則跳轉到:cond_**
"if-gt vA, vB, :cond_**"   如果vA大於vB則跳轉到:cond_**
"if-le vA, vB, :cond_**"    如果vA小於等於vB則跳轉到:cond_**
"if-eqz vA, :cond_**"   如果vA等於0則跳轉到:cond_**
"if-nez vA, :cond_**"   如果vA不等於0則跳轉到:cond_**
"if-ltz vA, :cond_**"    如果vA小於0則跳轉到:cond_**
"if-gez vA, :cond_**"   如果vA大於等於0則跳轉到:cond_**
"if-gtz vA, :cond_**"   如果vA大於0則跳轉到:cond_**
"if-lez vA, :cond_**"    如果vA小於等於0則跳轉到:cond_**

 z 既可以表示zero(0) 也可以是null、或者false;具體看邏輯。

 四、switch分支語句

 1 .method private packedSwitch(I)Ljava/lang/String;
 2     .locals 1
 3     .parameter "i"
 4     .prologue
 5     .line 21
 6     const/4 v0, 0x0
 7     .line 22
 8     .local v0, str:Ljava/lang/String;  #v0為字符串,0表示null
 9     packed-switch p1, :pswitch_data_0  #packed-switch分支,pswitch_data_0指定case區域
10     .line 36
11     const-string v0, "she is a person"  #default分支
12     .line 39
13     :goto_0      #所有case的出口
14     return-object v0 #返回字符串v0
15     .line 24
16     :pswitch_0    #case 0
17     const-string v0, "she is a baby"
18     .line 25
19     goto :goto_0  #跳轉到goto_0標號處
20     .line 27
21     :pswitch_1    #case 1
22     const-string v0, "she is a girl"
23     .line 28
24     goto :goto_0  #跳轉到goto_0標號處
25     .line 30
26     :pswitch_2    #case 2
27     const-string v0, "she is a woman"
28     .line 31
29     goto :goto_0  #跳轉到goto_0標號處
30     .line 33
31     :pswitch_3    #case 3
32     const-string v0, "she is an obasan"
33     .line 34
34     goto :goto_0  #跳轉到goto_0標號處
35     .line 22
36     nop
37     :pswitch_data_0
38     .packed-switch 0x0    #case  區域,從0開始,依次遞增
39         :pswitch_0  #case 0
40         :pswitch_1  #case 1
41         :pswitch_2  #case 2
42         :pswitch_3  #case 3
43     .end packed-switch
44 .end method

packed-switch 指令。p1為傳遞進來的 int 類型的數值,pswitch_data_0 為case 區域,在 case 區域中,第一條指令“.packed-switch”指定了比較的初始值為0 ,pswitch_0~ pswitch_3分別是比較結果為“case 0 ”到“case 3 ”時要跳轉到的地址。可以發現,標號的命名采用 pswitch_ 開關,后面的數值為 case 分支需要判斷的值,並且它的值依次遞增。再來看看這些標號處的代碼,每個標號處都使用v0 寄存器初始化一個字符串,然后跳轉到了goto_0 標號處,可見goto_0 是所有的 case 分支的出口。另外,“.packed-switch”區域指定的case 分支共有4 條,對於沒有被判斷的 default 分支,會在代碼的 packed-switch指令下面給出。

 至此,有規律遞增的 switch 分支就算是搞明白了。最后,將這段 smali 代碼整理為Java代碼如下。

 1 private String packedSwitch(int i) {
 2     String str = null;
 3     switch (i) {
 4         case 0:
 5             str = "she is a baby";
 6             break;
 7         case 1:
 8             str = "she is a girl";
 9             break;
10         case 2:
11             str = "she is a woman";
12             break;
13         case 3:
14             str = "she is an obasan";
15             break;
16         default:
17             str = "she is a person";
18             break;
19     }
20     return str;
21 }

現在我們來看看無規律的case 分支語句代碼會有什么不同

 1 .method private sparseSwitch(I)Ljava/lang/String;
 2     .locals 1
 3     .parameter "age"
 4     .prologue
 5     .line 43
 6     const/4 v0, 0x0
 7     .line 44
 8     .local v0, str:Ljava/lang/String;
 9     sparse-switch p1, :sswitch_data_0  # sparse-switch分支,sswitch_data_0指定case區域
10     .line 58
11     const-string v0, "he is a person"  #case default
12     .line 61
13     :goto_0    #case 出口
14     return-object v0  #返回字符串
15     .line 46
16     :sswitch_0    #case 5
17     const-string v0, "he is a baby"
18     .line 47
19     goto :goto_0 #跳轉到goto_0標號處
20     .line 49
21     :sswitch_1    #case 15
22     const-string v0, "he is a student"
23     .line 50
24     goto :goto_0 #跳轉到goto_0標號處
25     .line 52
26     :sswitch_2    #case 35
27     const-string v0, "he is a father"
28     .line 53
29     goto :goto_0 #跳轉到goto_0標號處
30     .line 55
31     :sswitch_3    #case 65
32     const-string v0, "he is a grandpa"
33     .line 56
34     goto :goto_0 #跳轉到goto_0標號處
35     .line 44
36     nop
37     :sswitch_data_0
38     .sparse-switch            #case 區域
39         0x5 -> :sswitch_0     #case 5(0x5)
40         0xf -> :sswitch_1     #case 15(0xf)
41         0x23 -> :sswitch_2    #case 35(0x23)
42         0x41 -> :sswitch_3    #case 65(0x41)
43     .end sparse-switch
44 .end method

按照分析packed-switch 的方法,我們直接查看 sswitch_data_0 標號處的內容。可以看到“.sparse-switch ”指令沒有給出初始case 的值,所有的case 值都使用“case 值 -> case 標號”的形式給出。此處共有4 個case ,它們的內容都是構造一個字符串,然后跳轉到goto_0 標號處,代碼架構上與packed-switch 方式的 switch 分支一樣。

 最后,將這段smali 代碼整理為Java 代碼如下。

 1 private String sparseSwitch(int age) {
 2     String str = null;
 3     switch (age) {
 4         case 5:
 5             str = "he is a baby";
 6             break;
 7         case 15:
 8             str = "he is a student";
 9             break;
10         case 35:
11             str = "he is a father";
12             break;
13         case 65:
14             str = "he is a grandpa";
15             break;
16         default:
17             str = "he is a person";
18             break;
19     }
20     return str;
21 }

五、try/catch 語句

  1 .method private tryCatch(ILjava/lang/String;)V
  2     .locals 10
  3     .parameter "drumsticks"
  4     .parameter "peple"
  5     .prologue
  6     const/4 v9, 0x0
  7                                                                                                                                                               .line 19
  8     try_start_0                                                              # 第1個try開始
  9     invoke-static {p2}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I   #將第2個參數轉換為int 型
 10     :try_end_0                                                               # 第1個try結束
 11     .catch Ljava/lang/NumberFormatException; {:try_start_0 .. :try_end_0} : catch_1  # catch_1
 12      move-result v1          #如果出現異常這里不會執行,會跳轉到catch_1標號處
 13                                                                                                                                                                .line 21
 14     .local v1, i:I            #.local聲明的變量作用域在.local聲明與.end local 之間
 15    :try_start_1               #第2個try 開始
 16     div-int v2, p1, v1        # 第1個參數除以第2個參數
 17     .line 22
 18     .local v2, m:I    
 19     mul-int v5, v2, v1        #m * i
 20     sub-int v3, p1, v5        #v3  = p1 - v5
 21     .line 23
 22     .local v3, n:I
 23     const-string v5, "\u5171\u6709%d\u53ea\u9e21\u817f\uff0c%d
 24         \u4e2a\u4eba\u5e73\u5206\uff0c\u6bcf\u4eba\u53ef\u5206\u5f97%d
 25         \u53ea\uff0c\u8fd8\u5269\u4e0b%d\u53ea"   # 格式化字符串
 26     const/4 v6, 0x4
 27     new-array v6, v6, [Ljava/lang/Object;
 28     const/4 v7, 0x0
 29     .line 24
 30     invoke-static {p1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
 31     move-result-object v8
 32     aput-object v8, v6, v7
 33     const/4 v7, 0x1
 34     invoke-static {v1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
 35     move-result-object v8
 36     aput-object v8, v6, v7
 37     const/4 v7, 0x2
 38     invoke-static {v2}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
 39     move-result-object v8
 40     aput-object v8, v6, v7
 41     const/4 v7, 0x3
 42     invoke-static {v3}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
 43     move-result-object v8
 44  
 45     aput-object v8, v6, v7
 46     .line 23
 47     invoke-static {v5, v6}, Ljava/lang/String;
 48         ->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
 49     move-result-object v4
 50     .line 25
 51     .local v4, str:Ljava/lang/String;
 52     const/4 v5, 0x0
 53     invoke-static {p0, v4, v5}, Landroid/widget/Toast;
 54         ->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)
 55         Landroid/widget/Toast;
 56     move-result-object v5
 57     invoke-virtual {v5}, Landroid/widget/Toast;->show()V # 使用Toast 顯示格式化后的結果
 58    :try_end_1                                            #第2個try 結束
 59     .catch Ljava/lang/ArithmeticException; {:try_start_1 .. :try_end_1} : catch_0    # catch_0
 60     .catch Ljava/lang/NumberFormatException; {:try_start_1 .. :try_end_1} : catch_1  # catch_1
 61     .line 33
 62     .end local v1           #i:I
 63     .end local v2           #m:I
 64     .end local v3           #n:I
 65     .end local v4           #str:Ljava/lang/String;
 66     :goto_0 
 67     return-void             # 方法返回
 68     .line 26 
 69     .restart local v1       #i:I
 70     :catch_0   
 71     move-exception v0
 72     .line 27
 73     .local v0, e:Ljava/lang/ArithmeticException;
 74                                                                                                                                                               :try_start_2                                       #第3個try 開始
 75     const-string v5, "\u4eba\u6570\u4e0d\u80fd\u4e3a0"           #“人數不能為0”
 76     const/4 v6, 0x0
 77     invoke-static {p0, v5, v6}, Landroid/widget/Toast;
 78         ->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)
 79         Landroid/widget/Toast;
 80     move-result-object v5
 81     invoke-virtual {v5}, Landroid/widget/Toast;->show()V         #使用Toast 顯示異常原因
 82     :try_end_2                                                   #第3個try 結束
 83     .catch Ljava/lang/NumberFormatException; {:try_start_2 .. :try_end_2} :catch_1
 84      goto :goto_0 #返回
 85     .line 29
 86     .end local v0           #e:Ljava/lang/ArithmeticException;
 87     .end local v1           #i:I
 88     :catch_1 
 89     move-exception v0
 90     .line 30
 91     .local v0, e:Ljava/lang/NumberFormatException;
 92     const-string v5, "\u65e0\u6548\u7684\u6570\u503c\u5b57\u7b26\u4e32" 
 93     #“無效的數值字符串”
 94     invoke-static {p0, v5, v9}, Landroid/widget/Toast;
 95         ->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)
 96         Landroid/widget/Toast;
 97     move-result-object v5
 98     invoke-virtual {v5}, Landroid/widget/Toast;->show()V # 使用Toast 顯示異 常原因
 99     goto :goto_0                                         #返回
100 .end method
代碼中的try語句塊使用try_start_開頭的標號注明,以try_end_開頭的標號結束。第一個try語句的開頭標號為try_start_0,結束標號為 try_end_0。使用多個try語句塊時標號名稱后面的數值依次遞增,本實例代碼中最多使用到了try_end_2。

在try_end_0 標號下面使用“.catch”指令指定處理到的異常類型與catch的標號,格式如下。

.catch < 異常類型> {<try起始標號> .. <try 結束標號>} <catch標號>

查看catch_1標號處的代碼發現,當轉換 String 到int 時發生異常會彈出“無效的數值字符串”的提示。對於代碼中的漢字,baksmali 在反編譯時將其使用Unicode進行編碼,因此,在閱讀前需要使用相關的編碼轉換工具進行轉換。

仔細閱讀代碼會發現在try_end_1標號下面使用“.catch”指令定義了 catch_0與catch_1兩個catch。catch_0標號的代碼開頭又有一個標號為try_start_2的try 語句塊,其實這個try語句塊是虛構的,假如下面的代碼。

 1 private void a() {
 2     try {
 3         ……
 4         try {
 5             ……
 6         } catch (XXX) {
 7             ……
 8         }
 9     } catch (YYY) {
10         ……
11     }      
12 }
當執行內部的try語句時發生了異常,如果異常類型為XXX,則內部catch就會捕捉到並執行相應的處理代碼,如果異常類型不是 XXX,那么就會到外層的 catch中去查找異常處理代碼,這也就是為什么實例的try_end_1標號下面會有兩個catch的原因,另外,如果在執行XXX異常的處理代碼時又發生了異常,這個時候該怎么辦?此時這個異常就會擴散到外層的catch中去,由於XXX異常的外層只有一個YYY的異常處理,這時會判斷發生的異常是否為YYY類型,如果是就會進行處理,不是則拋給應用程序。回到本實例中來,如果在執行內部的ArithmeticException異常處理時再次發生別的異常,就會調用外層的 catch進行異常捕捉,因此在try_end_2標號下面有一個 catch_1就很好理解了。
最后,將這段 smali 代碼整理為Java 代碼如下。
 1  private void tryCatch(int drumsticks, String peple) {
 2         try {
 3             int i = Integer.parseInt(peple);
 4             try {
 5                 int m = drumsticks / i;
 6                 int n = drumsticks - m * i;
 7                 String str = String.format("共有%d只雞腿,%d個人平分,每人可分得%d只,還剩下%d只",drumsticks, i, m, n);
 8                 Toast.makeText(MainActivity.this, str,Toast.LENGTH_SHORT).show();
 9             } catch (ArithmeticException e) {
10                 Toast.makeText(MainActivity.this, " 人數不能為0",Toast.LENGTH_SHORT).show();
11             }
12         } catch (NumberFormatException e) {
13             Toast.makeText(MainActivity.this, " 無效的數值字符串",Toast.LENGTH_SHORT).show();
14         }        
15 }
finally語句塊
源碼:
1    try {
2             ServerSocket serverSocket= new ServerSocket(10000);
3             Socket socket=serverSocket.accept();
4         } catch (IOException e) {
5             e.printStackTrace();
6         }finally{
7            int abc=5;
8            Toast.makeText(this, "sssss ", Toast.LENGTH_SHORT).show();
9     }

finally 語句塊作用:執行一些必要代碼。即不管出現異常與否,在finally中的代碼都會被執行
執行時機:針對所有catch語句之后,退出方法之前將被執行(即先執行catch里面的代碼,但在throw之前將轉向finally)。finally中返回的結果將可以覆蓋catch中返回的結果
對應的smail代碼如下: 

 1 :try_start_0
 2     new-instance v2, Ljava/net/ServerSocket;        #ServerSocket v2 = null;   
 3     const/16 v3, 0x2710                             # v3 = 10000;
 4     invoke-direct {v2, v3}, Ljava/net/ServerSocket;-><init>(I)V  # v2 = new ServerSocket(v3);
 5     .line 21
 6     .local v2, serverSocket:Ljava/net/ServerSocket;
 7     invoke-virtual {v2}, Ljava/net/ServerSocket;->accept()Ljava/net/Socket; # v2.accept( );
 8     :try_end_0
 9     .catchall {:try_start_0 .. :try_end_0} :catchall_0  
10 //上一句處理start_0對應的異常塊是catchall_0   也就是finally
11     .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0
12 //上一句處理start_0對應的異常塊是catch_0,catch_0異常塊先執行,之后再執行catchall_0  

相對應的smali代碼為: 

 1 :try_start_0
 2     new-instance v2, Ljava/net/ServerSocket;
 3     const/16 v3, 0x2710
 4     invoke-direct {v2, v3}, Ljava/net/ServerSocket;-><init>(I)V
 5     .line 21
 6     .local v2, serverSocket:Ljava/net/ServerSocket;
 7     invoke-virtual {v2}, Ljava/net/ServerSocket;->accept()Ljava/net/Socket;
 8     :try_end_0
 9 
10     .catchall {:try_start_0 .. :try_end_0} :catchall_0
11     .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0
12     .line 27
13     const/4 v0, 0x5      #正常流程 即未發生異常
14     .line 28
15     .local v0, abc:I
16     const-string v3, "sssss "
17     invoke-static {p0, v3, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
18     move-result-object v3
19     invoke-virtual {v3}, Landroid/widget/Toast;->show()V
20     .line 32
21     .end local v2        #serverSocket:Ljava/net/ServerSocket;
22     :goto_0
23     return-void
24     .line 22
25     .end local v0        #abc:I
26   
27    :catch_0              #當發生異常時執行
28     move-exception v1
29     .line 24
30     .local v1, e:Ljava/io/IOException;
31     
32     :try_start_1
33       invoke-virtual {v1}, Ljava/io/IOException;->printStackTrace()V
34     :try_end_1
35     .catchall {:try_start_1 .. :try_end_1} :catchall_0    #異常部分執行完畢,轉而執行finally
36   
37     .line 27
38     const/4 v0, 0x5
39     .line 28
40     .restart local v0       #abc:I
41     const-string v3, "sssss "
42     invoke-static {p0, v3, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
43     move-result-object v3
44     invoke-virtual {v3}, Landroid/widget/Toast;->show()V
45     goto :goto_0
46     .line 25
47     .end local v0           #abc:I
48     .end local v1           #e:Ljava/io/IOException;
49     
50   #finally代碼定義部分
51     :catchall_0
52     move-exception v3
53     .line 27
54     const/4 v0, 0x5
55     .line 28
56     .restart local v0       #abc:I
57     const-string v4, "sssss "
58     invoke-static {p0, v4, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
59     move-result-object v4
60     invoke-virtual {v4}, Landroid/widget/Toast;->show()V
61     .line 30
62     throw v3

六、for循環

  1 # virtual methods
  2 .method public onClick(Landroid/view/View;)V
  3 .locals 9
  4 .parameter “v”
  5 
  6 .prologue
  7 .line 36
  8 invoke-virtual {p1}, Landroid/view/View;->getId()I  # 非靜態方法參數中隱含的第一個參數p0為this指針, p1為第一個參數, 即View對象
  9 
 10 move-result v6     # 把上次的計算結果給第七個寄存器,v6=p1.getId(), v6中為View對象的id
 11 
 12 packed-switch v6, :pswitch_data_0    # switch(v6)
 13 
 14 # —————– 程序出口開始 ——————
 15 .line 58
 16 :goto_0         # for循環出口
 17 return-void     # return;
 18 # —————– 程序出口結束 ——————
 19 
 20 # —————– 獲取控件內容開始 ——————
 21 .line 39
 22 :pswitch_0
 23 iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;  # v6保存this指針
 24 
 25 const v7, 0x7f080001   # v7 = txtValue1, 該id保存在public.xml中
 26 
 27 invoke-virtual {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->findViewById(I)Landroid/view/View; # findViewById(txtValue1)
 28 
 29 move-result-object v4  # v4為txtValue1對應的View對象
 30 
 31 check-cast v4, Landroid/widget/EditText;  # 將View對象轉換成EditText, 完成后v4中是txtValue1對象, 失敗會拋出ClassCastException異常
 32 
 33 .line 40
 34 .local v4, txtValue1:Landroid/widget/EditText;
 35 iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;
 36 
 37 const v7, 0x7f080003  # v7 = txtValue2
 38 
 39 invoke-virtual {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->findViewById(I)Landroid/view/View;
 40 
 41 move-result-object v5  # v5為txtValue2對應的View對象
 42 
 43 check-cast v5, Landroid/widget/EditText;  # 將View對象轉換成EditText, 完成后v5中是txtValue2對象
 44 
 45 .line 41
 46 .local v5, txtValue2:Landroid/widget/EditText;
 47 invoke-virtual {v4}, Landroid/widget/EditText;->getText()Landroid/text/Editable;    # 根據.line 39處可知,v4中為txtValue1對象
 48 
 49 move-result-object v6   # v6 = txtValue1.getText();
 50 
 51 invoke-interface {v6}, Landroid/text/Editable;->toString()Ljava/lang/String;
 52 
 53 move-result-object v6   # v6 = txtValue1.getText().toString();
 54 
 55 invoke-static {v6}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I
 56 
 57 move-result v1    # v1 = Integer.parseInt(v6); 也就是起始數值
 58 
 59 .line 42
 60 .local v1, from:I
 61 invoke-virtual {v5}, Landroid/widget/EditText;->getText()Landroid/text/Editable;    # 根據.line 40處可知,v5中為txtValue2對象
 62 
 63 move-result-object v6  # v6 = txtValue2.getText();
 64 
 65 invoke-interface {v6}, Landroid/text/Editable;->toString()Ljava/lang/String;
 66 
 67 move-result-object v6  # v6 = txtValue2.getText().toString();
 68 
 69 invoke-static {v6}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I
 70 
 71 move-result v0    # v0 = Integer.parseInt(v6); 也就是結束數值
 72 
 73 # —————– 獲取控件內容結束 ——————
 74 
 75 .line 43
 76 .local v0, end:I
 77 if-le v1, v0, :cond_0   # if v1 <= v0, 即起始數值 <= 結束數值, 則跳到cond_0
 78 
 79 # —————– 起始數值 > 結束數值時開始 ——————
 80 .line 45
 81 iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;
 82 
 83 const-string v7, “\u8d77\u59cb\u6570\u503c\u4e0d\u80fd\u5927\u4e8e\u7ed3\u675f\u6570\u503c!”  # 起始數值不能大於結束數值
 84 
 85 #calls: Lcom/sanwho/crackdemo/ForActivity;->MessageBox(Ljava/lang/String;)V
 86 invoke-static {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->access$0(Lcom/sanwho/crackdemo/ForActivity;Ljava/lang/String;)V
 87 
 88 goto :goto_0
 89 
 90 # —————– 起始數值 > 結束數值時結束 ——————
 91 
 92 # —————– 起始數值 <= 結束數值時開始 —————–
 93 .line 49
 94 :cond_0
 95 const/4 v3, 0x0   # v3 = 0, 即int sum = 0;
 96 
 97 .line 50
 98 .local v3, sum:I
 99 move v2, v1       # v2 = v1, v2即源碼中的i變量
100 
101 .local v2, i:I
102 :goto_1           # for循環主要入口
103 if-le v2, v0, :cond_1   # if 當前數值 <= 結束數值, 跳到cond_1;  否則循環結束, 顯示累加結果
104 
105 .line 54
106 iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;  # v6指向MessageBox方法
107 
108 new-instance v7, Ljava/lang/StringBuilder;    # v7為StringBuilder對象
109 
110 const-string v8, “\u7d2f\u52a0\u7ed3\u679c\uff1a”  # v8 = “累加結果:”
111 
112 invoke-direct {v7, v8}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V  # 以v8為參數調用StringBuilder構造函數
113 
114 invoke-static {v3}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; # 把int型的sum值轉成字符串
115 
116 move-result-object v8   # v8 = Integer.toString(v3); 此時v8中為sum的值
117 
118 invoke-virtual {v7, v8}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;   # 把累加結果和sum的值進行追加
119 
120 move-result-object v7  # v7 為 “累加結果:” + Integer.toString(sum)的StringBuilder對象;
121 
122 invoke-virtual {v7}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;  # 將v7轉為字符串對象
123 
124 move-result-object v7   # v7 = “累加結果:” + Integer.toString(sum);
125 
126 #calls: Lcom/sanwho/crackdemo/ForActivity;->MessageBox(Ljava/lang/String;)V
127 invoke-static {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->access$0(Lcom/sanwho/crackdemo/ForActivity;Ljava/lang/String;)V  # 調用MessageBox顯示字符串
128 
129 goto :goto_0  # 跳到goto_0
130 # —————– 起始數值 <= 結束數值時結束 —————–
131 
132 .line 52
133 :cond_1           # 加1操作入口
134 add-int/2addr v3, v2   # v3 = v3 + v2, 即sum += i
135 
136 .line 50
137 add-int/lit8 v2, v2, 0x1  # v2 = v2 + 1, , 即i = i + 1
138 
139 goto :goto_1   # 跳到for循環入口繼續比對
140 
141 .line 36
142 nop
143 
144 :pswitch_data_0
145 .packed-switch 0x7f080004
146 :pswitch_0
147 .end packed-switch
148 .end method

源碼解釋

 1 Button.OnClickListener onClickListener = new Button.OnClickListener()
 2 {
 3     @Override
 4     public void onClick(View v)
 5     {
 6         switch (v.getId())
 7         {
 8         case R.id.btnSubmit:
 9             EditText txtValue1 = (EditText) findViewById(R.id.txtValue1);
10             EditText txtValue2 = (EditText) findViewById(R.id.txtValue2);
11             int from = Integer.parseInt(txtValue1.getText().toString());
12             int end = Integer.parseInt(txtValue2.getText().toString());
13             if (from > end){
14                 MessageBox("起始數值不能大於結束數值!");
15             }
16             else
17             {
18                  int sum = 0;
19                  for (int i = from; i <= end; i++){
20                     
21                      sum += i;
22                  }
23                  MessageBox("累加結果:" + Integer.toString(sum));
24             }
25             break;
26         }
27     }
28 };
29 
30 private void MessageBox(String str)
31 {        
32     Toast.makeText(this, str, Toast.LENGTH_LONG).show();
33 }

如果看不懂access$0或者this$0等請看下一章節

八、內部類

Java 語言允許在一個類的內部定義另一個類,這種在類中定義的類被稱為內部類(Inner Class)。內部類可分為成員內部類、靜態嵌套類、方法內部類、匿名內部類。前面我們曾經說過,baksmali 在反編譯dex 文件的時候,會為每個類單獨生成了一個 smali 文件,內部類作為一個獨立的類,它也擁有自己獨立的smali 文件,只是內部類的文件名形式為“[外部類]$[內部類].smali ”,例如下面的類。

1 class Outer {
2    class Inner{}
3 }

baksmali 反編譯上述代碼后會在同一目錄生成兩個文件:Outer.smali 與Outer$Inner.smali。

  1 public class MainActivity extends Activity {    
  2     private Button btnAnno;
  3     private Button btnCheckSN;
  4     private EditText edtSN;  
  5     @Override
  6     public void onCreate(Bundle savedInstanceState) {
  7         super.onCreate(savedInstanceState);
  8         setContentView(R.layout.activity_main);
  9         btnAnno = (Button) findViewById(R.id.btn_annotation);
 10         btnCheckSN = (Button) findViewById(R.id.btn_checksn);
 11         edtSN = (EditText) findViewById(R.id.edt_sn);
 12         btnAnno.setOnClickListener(new OnClickListener() {
 13            @Override
 14             public void onClick(View v) {
 15                 getAnnotations();                
 16             }
 17         });
 18         
 19         btnCheckSN.setOnClickListener(new OnClickListener() {
 20             @Override
 21             public void onClick(View v) {
 22                 SNChecker checker = new SNChecker(edtSN.getText().toString());
 23                 String str = checker.isRegistered() ? "注冊碼正確" : "注冊碼錯誤";
 24                 Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
 25             }
 26         });
 27     }
 28     
 29     private void getAnnotations() {
 30         try {
 31             Class<?> anno = Class.forName("com.droider.anno.MyAnno");
 32             if (anno.isAnnotationPresent(MyAnnoClass.class)) {
 33                 MyAnnoClass myAnno = anno.getAnnotation(MyAnnoClass.class);
 34                 Toast.makeText(this, myAnno.value(), Toast.LENGTH_SHORT).show();
 35             }
 36             Method method = anno.getMethod("outputInfo", (Class[])null);
 37             if (method.isAnnotationPresent(MyAnnoMethod.class)) {
 38                 MyAnnoMethod myMethod = method.getAnnotation(MyAnnoMethod.class);
 39                 String str = myMethod.name() + " is " + myMethod.age() + " years old.";
 40                 Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
 41             }
 42             Field field = anno.getField("sayWhat");
 43             if (field.isAnnotationPresent(MyAnnoField.class)) {
 44                 MyAnnoField myField = field.getAnnotation(MyAnnoField.class);
 45                 Toast.makeText(this, myField.info(), Toast.LENGTH_SHORT).show();
 46             }
 47         } catch (Exception e) {
 48             e.printStackTrace();
 49         }
 50     }
 51 
 52     @Override
 53     public boolean onCreateOptionsMenu(Menu menu) {
 54         getMenuInflater().inflate(R.menu.activity_main, menu);
 55         return true;
 56     }
 57     
 58     public class SNChecker {
 59         private String sn;
 60         public SNChecker(String sn) {
 61             this.sn = sn;
 62         }
 63         
 64         public boolean isRegistered() {
 65             boolean result = false;
 66             char ch = '\0';
 67             int sum = 0;
 68             if (sn == null || (sn.length() < 8)) return result;
 69             int len = sn.length();
 70             if (len == 8) {
 71                 ch = sn.charAt(0);
 72                 switch (ch) {
 73                     case 'a':
 74                     case 'f':
 75                         result = true;
 76                         break;
 77                     default:
 78                         result = false;
 79                         break;
 80                 }
 81                 if (result) {
 82                     ch = sn.charAt(3);
 83                     switch (ch) {
 84                         case '1':
 85                         case '2':
 86                         case '3':
 87                         case '4':
 88                         case '5':
 89                             result = true;
 90                             break;
 91                         default:
 92                             result = false;
 93                             break;
 94                     }
 95                 }
 96             } else if (len == 16) {
 97                 for (int i = 0; i < len; i++) {
 98                     char chPlus = sn.charAt(i);
 99                     sum += (int) chPlus;
100                 }
101                 result = ((sum % 6) == 0) ? true : false;
102             }
103             return result;
104         }
105     }
106 }

MainActivity$ SNChecker.smali 文件,這個SNChecker 就是MainActivity的一個內部類。打開這個文件,代碼結構如下。

  1 .class public Lcom/droider/crackme0502/MainActivity$SNChecker;
  2 .super Ljava/lang/Object;
  3 .source "MainActivity.java"
  4 
  5 
  6 # annotations
  7 .annotation system Ldalvik/annotation/EnclosingClass;
  8     value = Lcom/droider/crackme0502/MainActivity;
  9 .end annotation
 10 
 11 .annotation system Ldalvik/annotation/InnerClass;
 12     accessFlags = 0x1
 13     name = "SNChecker"
 14 .end annotation
 15 
 16 
 17 # instance fields
 18 .field private sn:Ljava/lang/String;
 19 
 20 .field final synthetic this$0:Lcom/droider/crackme0502/MainActivity;
 21 
 22 
 23 # direct methods
 24 .method public constructor <init>(Lcom/droider/crackme0502/MainActivity;Ljava/lang/String;)V
 25     .locals 0
 26     .parameter
 27     .parameter "sn"
 28 
 29     .prologue
 30     .line 83
 31     iput-object p1, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->this$0:Lcom/droider/crackme0502/MainActivity;
 32 
 33     invoke-direct {p0}, Ljava/lang/Object;-><init>()V
 34 
 35     .line 84
 36     iput-object p2, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
 37 
 38     .line 85
 39     return-void
 40 .end method
 41 
 42 
 43 # virtual methods
 44 .method public isRegistered()Z
 45     .locals 10
 46 
 47     .prologue
 48     const/16 v9, 0x8
 49 
 50     const/4 v7, 0x0
 51 
 52     .line 88
 53     const/4 v4, 0x0
 54 
 55     .line 89
 56     .local v4, result:Z
 57     const/4 v0, 0x0
 58 
 59     .line 90
 60     .local v0, ch:C
 61     const/4 v6, 0x0
 62 
 63     .line 91
 64     .local v6, sum:I
 65     iget-object v8, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
 66 
 67     if-eqz v8, :cond_0
 68 
 69     iget-object v8, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
 70 
 71     invoke-virtual {v8}, Ljava/lang/String;->length()I
 72 
 73     move-result v8
 74 
 75     if-ge v8, v9, :cond_1
 76 
 77     :cond_0
 78     move v5, v4
 79 
 80     .line 126
 81     .end local v4           #result:Z
 82     .local v5, result:I
 83     :goto_0
 84     return v5
 85 
 86     .line 92
 87     .end local v5           #result:I
 88     .restart local v4       #result:Z
 89     :cond_1
 90     iget-object v8, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
 91 
 92     invoke-virtual {v8}, Ljava/lang/String;->length()I
 93 
 94     move-result v3
 95 
 96     .line 93
 97     .local v3, len:I
 98     if-ne v3, v9, :cond_3
 99 
100     .line 94
101     iget-object v8, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
102 
103     invoke-virtual {v8, v7}, Ljava/lang/String;->charAt(I)C
104 
105     move-result v0
106 
107     .line 95
108     sparse-switch v0, :sswitch_data_0
109 
110     .line 101
111     const/4 v4, 0x0
112 
113     .line 104
114     :goto_1
115     if-eqz v4, :cond_2
116 
117     .line 105
118     iget-object v7, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
119 
120     const/4 v8, 0x3
121 
122     invoke-virtual {v7, v8}, Ljava/lang/String;->charAt(I)C
123 
124     move-result v0
125 
126     .line 106
127     packed-switch v0, :pswitch_data_0
128 
129     .line 115
130     const/4 v4, 0x0
131 
132     :cond_2
133     :goto_2
134     move v5, v4
135 
136     .line 126
137     .restart local v5       #result:I
138     goto :goto_0
139 
140     .line 98
141     .end local v5           #result:I
142     :sswitch_0
143     const/4 v4, 0x1
144 
145     .line 99
146     goto :goto_1
147 
148     .line 112
149     :pswitch_0
150     const/4 v4, 0x1
151 
152     .line 113
153     goto :goto_2
154 
155     .line 119
156     :cond_3
157     const/16 v8, 0x10
158 
159     if-ne v3, v8, :cond_2
160 
161     .line 120
162     const/4 v2, 0x0
163 
164     .local v2, i:I
165     :goto_3
166     if-lt v2, v3, :cond_4
167 
168     .line 124
169     rem-int/lit8 v8, v6, 0x6
170 
171     if-nez v8, :cond_5
172 
173     const/4 v4, 0x1
174 
175     :goto_4
176     goto :goto_2
177 
178     .line 121
179     :cond_4
180     iget-object v8, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
181 
182     invoke-virtual {v8, v2}, Ljava/lang/String;->charAt(I)C
183 
184     move-result v1
185 
186     .line 122
187     .local v1, chPlus:C
188     add-int/2addr v6, v1
189 
190     .line 120
191     add-int/lit8 v2, v2, 0x1
192 
193     goto :goto_3
194 
195     .end local v1           #chPlus:C
196     :cond_5
197     move v4, v7
198 
199     .line 124
200     goto :goto_4
201 
202     .line 95
203     :sswitch_data_0
204     .sparse-switch
205         0x61 -> :sswitch_0
206         0x66 -> :sswitch_0
207     .end sparse-switch
208 
209     .line 106
210     :pswitch_data_0
211     .packed-switch 0x31
212         :pswitch_0
213         :pswitch_0
214         :pswitch_0
215         :pswitch_0
216         :pswitch_0
217     .end packed-switch
218 .end method

發現它有兩個注解定義塊“Ldalvik/annotation/EnclosingClass;”與“Ldalvik/annotation/ InnerClass; ”、兩個實例字段sn 與this$0 、一個直接方法 init()、一個虛方法isRegistered() 。注解定義塊我們稍后進行講解。先看它的實例字段,sn 是字符串類型,this$0 是MainActivity類型,synthetic 關鍵字表明它是“合成”的,那 this$0 到底是個什么東西呢?

其實this$0 是內部類自動保留的一個指向所在外部類的引用。左邊的 this 表示為父類的引用,右邊的數值0 表示引用的層數。我們看下面的類。

1 public class Outer {    //this$0 
2      public class FirstInner {        //this$1 
3             public class SecondInner {      //this$2 
4                 public class ThirdInner {
5                 } 
6         } 
7     }
8 }

每往里一層右邊的數值就加一,如 ThirdInner類訪問 FirstInner 類的引用為this$1 。在生成的反匯編代碼中,this$X 型字段都被指定了synthetic 屬性,表明它們是被編譯器合成的、虛構的,代碼的作者並沒有聲明該字段。

我們再看看MainActivity$SNChecker的構造函數,看它是如何初始化的。代碼如下。

 1 # direct methods
 2 .method public constructor <init>(Lcom/droider/crackme0502/MainActivity;Ljava/lang/String;)V
 3     .locals 0
 4     .parameter       #第一個參數MainActivity引用
 5     .parameter "sn"  #第二個參數字符串sn
 6 
 7     .prologue
 8     .line 83
 9     #將MainActivity引用賦值給this$0
10     iput-object p1, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->this$0:Lcom/droider/crackme0502/MainActivity;
11 
12     #調用默認的構造函數
13     invoke-direct {p0}, Ljava/lang/Object;-><init>()V
14 
15     .line 84
16     #將sn字符串的值賦給sn字段
17     iput-object p2, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->sn:Ljava/lang/String;
18 
19     .line 85
20     return-void
21 .end method

對於一個非靜態的方法而言,會隱含的使用p0寄存器當作類的this 引用。因此,這里的確是使用了3 個寄存器:p0表示MainActivity$SNChecker自身的引用,p1表示MainActivity的引用,p2表示sn 字符串。另外,從 MainActivity$SNChecker的構造函數可以看出,內部類的初始化共有以下 3 個步驟:首先是保存外部類的引用到本類的一個 synthetic字段中,以便內部類的其它方法使用,然后是調用內部類的父類的構造函數來初始化父類,最后是對內部類自身進行初始化。

  

一個方法中指定的寄存器個
在一個方法(method)中有兩中方式指定有多少個可用的寄存器。指令.registers指令指定了在這個方法中有多少個可用的寄存器,指令.locals指明了在這個方法中非參(non-parameter)寄存器的數量。然而寄存器的總數也包括保存方法參數的寄存器。

參數是如何傳遞的?
當一個方法被調用時,該方法的參數被保存在最后N個寄存器中。如果一個方法有2個參數和5個寄存器(V0-V4),參數將被保存在最后的2個寄存器內V3和V4.

非靜態方法的第一個參數,總是被方法調用的對象。
例如,你寫了一個非靜態方法LMyObject;->callMe(II)V。這個方法有2個int參數,但在這兩個整型參數前面還有一個隱藏的參數LMyObject;所以這個方法總共有3個參數。

比如說,在方法中指定有5個寄存器(V0-V4),只用.register指令指定5個,或者使用.locals指令指定2個(2個local寄存器+3個參數寄存器)。該方法被調用的時候,調用方法的對象(即this引用)會保存在V2中,第一個參數在V3中,第二個參數在v4中。

除了不包含this隱藏參數,對於靜態方法都是相同的。

寄存器名稱
有兩種寄存器的命名方式,對於參數寄存器有普通的V命名方式和P命名方式。在方法(method)中第一個參數寄存器,是使用P方式命名的第一個寄存器,讓我們回到前面的例子中,有三個參數和5個寄存器,下面的這個表顯示了對每個寄存器的普通V命名方式,后面是P方式命名的參數寄存器。

v0   the first local register
v1   the second local register
v2 p0 the first parameter register
v3 p1 the second parameter register
v4 p2 the third parameter register

You can reference parameter registers by either name - it makes no difference.

你可以使用名稱引用參數寄存器,他們沒有區別。

引入參數寄存器的目的
P命名方式被引入去解決,在編輯smail代碼時候共同的煩惱。

假設你有一個方法(mehtod),這個方法帶有一些參數,並且你需要添加一些代碼到這個方法中,這時發現需要一些額外的寄存器,你會想“沒有什么大不了的。我只需要使用.registers指令添加寄存器數量就可以了。”
不幸的是沒有想象的那么容易,請記住,方法中方法的參數被保存在最后的寄存器里。如果你增加了寄存器的數量,達到讓寄存器中的參數被傳入的目的。所以你不得不使用.registers指令重新分配參數寄存器的編號。
但如果在方法中P命名方式,被用來引用參數寄存器。你將很容易的在方法中去修改寄存器數量,而不用去擔心現有寄存器的編號。

注意:在默認的baksmali中,參數寄存器將使用P命名方式,如果出於某種原因你要禁用P命名方式,而要強制使用V命名方式,應當使用-p/--no-parameter-registers選項。

Long/Double values
正如前面提到的,long和double類型都是64位,需要2個寄存器。當你引用參數的時候一定要記住,例如:你有一個非靜態方法LMyObject;->MyMethod(IJZ)V,LMyObject方法的參數為int、long、bool。所以這個方法的所有參數需要5個寄存器。

p0 this
p1 I
p2, p3 J
p4 Z

另外當你調用方法后,你必須在寄存器列表,調用指令中指明,兩個寄存器保存了double-wide寬度的參數。

  

關於幾個調用方法指令: invoke-virtual、invoke-direct、invoke-super介紹。

涉及到Java強大的動態擴展能力,這一特性使得可以在類運行期間才能確定某些目標方法的實際引用,稱為動態連接;也有一部分方法的符號引用在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜態解析。

在Java語言中,符合“編譯器可知,運行期不可變”這個要求的方法主要有靜態方法和私有方法兩大類,前者與類型直接關聯,后者在外部不可被訪問,這兩種方法都不可能通過繼承或別的方式重寫出其他的版本,因此它們都適合在類加載階段進行解析。 

  • invoke-static 是類靜態方法的調用,編譯時,靜態確定的;
  • invoke-virtual 虛方法調用,調用的方法運行時確認實際調用,和實例引用的實際對象有關,動態確認的,一般是帶有修飾符protected或public的方法;
  • invoke-direct 沒有被覆蓋方法的調用,即不用動態根據實例所引用的調用,編譯時,靜態確認的,一般是private或<init>方法;
  • invoke-super 直接調用父類的虛方法,編譯時,靜態確認的。
  • invokeinterface 調用接口方法,調用的方法運行時確認實際調用,即會在運行時才確定一個實現此接口的對象。

 

參考:

http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

http://www.cnblogs.com/Fang3s/p/3782903.html

http://www.52pojie.cn/thread-233852-1-1.html

http://book.2cto.com/201212/12474.html

http://book.2cto.com/201212/12475.html

https://blog.csdn.net/l25000/article/details/46842013

https://blog.csdn.net/l25000/article/details/46891799


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM