张墨轩的技术宅

不忘初心,方得始终

关于ADB的一些有趣认知

当你有一台PC机假设是Windows系统,和一台android手机,你想让两者之间进行交流该怎么办呢?我们首先想到的肯定是通过wifi将二个设备连接到同一个局域网内,或者直接wifi-direct,但是可能的情况是环境里没有wifi,或者PC机压根就没有网卡等等情况,导致wifi连接并不可行。所以android手机和PC机默认连接方式是通过USB线缆将二者连接在一起,通过USB进行通讯。
OK,说到这里既然物理的通讯链路已经解决了,那么站在软件的角度来说是个什么样子呢?
ADB由两个物理文件组成:
     1. adb或adb.exe,运行于PC端,包括Linux、Windows、Mac OS等系统之中,通常是x86架构上。
     2. adbd,运行于Android设备的底层Linux之中,ARMv5架构上。
从运行实体角度来看adb主要分为三部分:
     1. adb client  (在adb.exe中实现,运行在PC机上)
     2. adb server (在adb.exe中实现,运行在PC机上)
     3. adbd (在adbd中实现,是一个linux deamon程序,它实现了adb service的功能,运行在android手机上)
交互流程:
      adb client  <==> adb server <==> adbd 
      [PC端]                 [PC端]                [android手机端]
下面为了行文方便,PC端统称为adb, 手机端称为adbd.
PC机上的adb(打开PC机的usb接口)与android上的adbd(打开手机的usb接口)进行通讯,从而达到PC机控制手机的目的。
我们来看看adb的功能,比较常用的功能如下:
第一:交互式shell。可以在PC机上登录到android手机(本质是linux),对android手机进行控制,就像通过ssh远程登录到linux下进行操作一样。其实原理上就是PC上的adb请求android手机上的adbd执行命令,然后把命令的执行结果返回给PC机上的adb,当然中间的通讯数据肯定是通过USB线缆进行传输的。
第二:文件传输。可以将PC上的文件push到手机上,也可以将手机上的文件pull到PC上。当然文件传输数据包同样是走的USB线缆。
第三:安装APK。原理上就是PC机首先把APK文件push到手机上,然后PC发<安装APK>的shell命令给adbd,说穿了还是adb与adbd通讯,通过USB线缆。
第四:调试辅助功能。这个功能是最让新手迷糊的一个功能,也是相对比较难理解的部分,所以要重点说说。adb是android debug bridge的缩写,证明这个程序本身就是以调试为重点的,既然是bridge,那就应当充当一个桥接的作用,事实上adb确实是起到了桥接的作用,接下来我们就来讨论一下。我们知道android上主要由两大类程序构成,JAVA程序和原生C/C++程序。下面分别讨论:

1. JAVA程序调试,了解JAVA调试体系的人都知道 JPDA(Java Platform Debugger Architecture),它是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI)。
其中JDI由调试前端(front-end)实现,比如jdb工具,比如eclipseIDE,它的两个插件org.eclipse.jdt.debug.ui和org.eclipse.jdt.debug与其强大的调试功能密切相关,其中前者是eclipse调试工具界面的实现,而后者则是JDI的一个完整实现。JVMTI由后端(back-end)实现,比如android中的Davlik虚拟机就实现了JVMTI。 说穿了调试过程就是运行在PC上的前端(如jdb工具,eclipseIDE等) 与 运行在android手机上的后端(Davlik虚拟机)之间通过网络进行通信,通信用的协议就是JDWP。交互如下:

    jdb,eclipseIDE等 <==网络通信,用的JDWP协议==>  Davlik虚拟机 <==调试==> JAVA程序
   [PC机中]                                           [android手机中] 
从运行角度讲就是PC机中的jdb,eclipseIDE等进程与android手机中执行JAVA程序的Davlik虚拟机进程之间通过网络socket进行通信,其中Davlik虚拟机进程会bind并且listen某个端口作为调试通信用,而jdb,eclipseIDE等进程会去connect这个端口。

2.  原生C/C++程序,像有些APK中的so文件,android中的各种deamon进程都是属于这种,调试原生C/C++程序需要用到GDB,同样分为前端和后端,比如NDK中包含的
\toolchains\arm-linux-androideabi-4.6\prebuilt\windows\bin\arm-linux-androideabi-gdb.exe 就是一个前端 运行在PC机上,
而\prebuilt\android-arm\gdbserver\gdbserver就是一个后端,运行在android手机上,它是真正的调试器,用于调试原生C/C++程序。

运行在PC机上的arm-linux-androideabi-gdb.exe 与 运行在android手机上gdbserver 之间通过网络socket进行通信,其中gdbserver进程会bind并且listen某个端口
作为调试通信用,arm-linux-androideabi-gdb.exe进程会去connect这个端口。交互如下:

    arm-linux-androideabi-gdb.exe <==网络通信==>  gdbserver  <==调试==> 原生C/C++程序
   [PC机中]                                     [android手机中]        [android手机中] 

根据上面对JAVA程序调试和原生C/C++程序调试的分析我们可以得知,不管是哪种程序的调试,说的直白些,本质上就是 运行在PC中的A进程 与 运行在android手机中的B进程 之间的网络socket通信的过程。 既然本质是网络socket通讯,那问题就来了,前面已经说过PC和手机之间是通过USB线缆连接的,那怎么样才能进行网络socket通讯呢?ADB使用了一个巧妙的方法用USB模拟了网络通讯。

比如命令:
adb forward tcp:6100 tcp:7100 // PC上所有6100端口通信数据将被重定向到手机端7100端口server上

这个时候adb server会bind和listen 6100这个端口,PC上的其它应用程序,比如A程序,可以connect localhost:6100 ,adb server就会将这个连接请求通过USB线缆传给手机端
的adbd,adbd就会去connect localhost:7100,手机端的listen 7100端口的 B程序就会接到连接请求。交互如下:
A程序 ==连接本机6100==> adb server ==USB线缆==> adbd ==连接本机7100==> B程序
[--------------PC机中---------------]    [----------android手机中----------]
看上去就好像 A程序 直接与 B程序 通讯一样。
这样adb就完成了bridge的功能,通过USB线缆将原本不能进行socket连接的PC与手机,巧妙的桥接到了一起。
这种方法还有个特点:就是多条连接可以复用一条链路
比如android手机中 有三个程序 B1,B2,B3 分别listen 8001 8002 8003
那么我们可以
adb forward tcp:7001 tcp:8001 
adb forward tcp:7002 tcp:8002 
adb forward tcp:7003 tcp:8003
然后 PC机器中 假设有三个程序A1,A2,A3:
程序A1=>connect localhost:7001=>adb server=USB=>adbd=>connect localhost:8001=>B1
程序A2=>connect localhost:7002=>adb server=USB=>adbd=>connect localhost:8002=>B2
程序A3=>connect localhost:7003=>adb server=USB=>adbd=>connect localhost:8003=>B3
虽然实际有三条链路,但是却是共享了一条USB链路进行通信。
文章看到这里,估计就有很多小伙伴会在想,既然一切都是为了网络socket通信,那么如果PC机和手机已经连接到了同一个局域网内,双方之间完全可以直接进行网络socket通信了,是不是就不用那么麻烦了,是不是adb都可以不要了。从某种意义上来说,在这样的情况下 确实可以不用adb了,交互式shell可以直接在手机上装一个ssh server,然后PC上直接ssh,windows下还可以用SecureCRT等工具,文件传输可以直接SCP,调试直接可以用jdb,gdb和手机进行连接调试。事实上互联网上这方面的资料也有很多。但是你如果直接用eclipseIDE的话那还是需要adb,因为eclipseIDE内部会使用adb的功能。既然双方之间已经可以连通,这个时候adb如果再用USB线缆的话感觉有点多此一举,能不能不用USB线缆呢?答案是肯定的。adb支持用wifi连接代替USB连接。首先打开手机的wifi设置,使其连接到网络。然后,需要在手机上对adb连接端口进行设
置,这里需要有root权限的终端(terminal)应用,这种类型的应用在各个Market都有不少,选择一个适合的就可以了。
然后,在手机中打开这个终端(terminal)应用,比如输入如下命令:
        su
        setprop service.adb.tcp.port 5555
        stop adbd
        start adbd
接着,可以查看一下你的手机的IP地址,最后,在你的PC上,进入到<Android-SDK>/platforms目录,运行如下命令:
adb connect 手机的IP地址:5555 (例如:adb connect 192.168.1.115:5555)
如果前面的配置正确无误,则可以得到:connected to 192.168.1.115:5555的输出。
这样,就可以使用adb来对手机进行调试了,此时也可以在Eclipse的设备列表中看到已经连接的手机设备了。

从进程的角度来说 这个就是 运行在PC端的adb.exe进程和运行在手机端的adbd进程之间通过wifi建立的一条socket链路而已。但是站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 不管adb和adbd之间是通过何种模式通信,他们之间的行为都是一致的,不论是USB线缆链接,还是wifi socket链接。
最后,我们来看看android模拟器,在没有真实手机的情况下可以用android模拟器来调试程序,那么android模拟器和真实的android手机之间在连接方式上有什么区别呢?
android模拟器是qemu-base的,它运行起来后会绑定二个端口,绑定端口会从TCP:5554端口开始,比如机器上运行了2个模拟器,那么先运行的模拟器将绑定5554,5555二个端口,第二个运行的模拟器将绑定5556,5557二个端口,假如有第三个,第四个模拟器要运行就以此类推。其中奇数类的端口就是用于adb连接的。比如PC机上的ADB会去连接第一个模拟器进程的5555的端口,从而建立连接。同理,站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 
这里顺带讲讲偶数类的端口,比如模拟器绑定的5554端口,这个端口是用于控制台的,ddms会通过这个端口连接上模拟器,这样程序员可以通过ddms动态的设置模拟器中的电话状态,比如模拟一个电话呼入,可以设置GPS的数值,从而模拟手机在移动等等。


软件生产流程

一. 代码的编写(操作系统平台,工具), HOST
二. 代码的编译(操作系统平台,工具), HOST
三. 程序的部署(工具,方法), HOST==>TARGET
四. 调试运行(操作系统平台,工具), TARGET
五. 软件的签名,发布,推广(更偏向业务,而不是纯粹技术)
典型的开发类型:
一. exe (pc机windows系统上跑)
二. app (苹果手机IOS系统上跑), 分为标准开发(没有越狱)和越狱开发(可以使用各种奇淫异技)二种类型,开发语言包括Objective-C,SWIFT,C/C++
三. apk (android手机android系统上跑) ,分为标准开发(没有root)和root开发(可以使用各种奇淫异技)二种类型, 开发语言包括JAVA,C/C++
四. arm-linux操作系统移植 (arm硬件板子上跑)


ARM学习的一些经验

我将ARM知识分成四个层次:
一:ARM架构
    先上一个简表:
架构处理器家族
ARMv1ARM1
ARMv2ARM2ARM3
ARMv3ARM6, ARM7
ARMv4StrongARMARM7TDMIARM9TDMI
ARMv5ARM7EJARM9EARM10EXScale
ARMv6ARM11ARM Cortex-M
ARMv7ARM Cortex-AARM Cortex-MARM Cortex-R
ARMv8Cortex-A50[9]
可见每一种架构都有若干种具体的实现,对ARM架构的学习主要是学习ARM指令,内存模型,中断模型等架构相关的知识,这个时候并没有牵扯到外设或者外设通讯协议啥的.
二:ARM内核
    比如ARM9系列(基于ARMv4,ARMv5架构). ARM内核是基于某个ARM架构版本下的具体实现,主要区别在于性能还有是否支持相关特性如MMU,Jazelle,SIMD,Thumb-2,VFP
等等.
三:基于ARM核的SOC或者MCU
     一般来说把面向高端的,面向应用处理的叫做SOC,把面向低端的,主要面向单片机市场的叫做MCU,下面为了行文方便 统一用SOC来代替.
比如三星的S3C2410就是一块包含了ARM920T内核的SOC,框图如下:

它的外形看起来就是一块芯片,和一块CPU长的差不多,所以很多人习惯叫它CPU,其实从严格意义上来说叫它CPU并不准确,它还包括了各种外设控制芯片功能,比如USB,SPI,IIS,LCD,FLASH,DMA,UART等控制器,你可以将它理解为 将CPU还有各种外设控制芯片全部做到了这块小小的芯片上,所以它并不只是一个CPU而已,而是一套片上系统,所以我们称呼这块芯片叫做SOC(片上系统)更加恰当. 对于SOC的学习,一般是首先要选定一块特定的产品比如S3C2410进行学习,而仅仅学习ARM内核知识是远远不够的,还必须要学习各种外设控制的方法和与各种外设通讯的协议等等.

四.基于SOC的板上系统.
  这个就是我们常常见到的电路板了,比如将各种外设如RAM,FLASH,LCD等,加上SOC如S3C2410,按设计焊接到一块PCB板上,调试好电路并且刷好程序就可以开始工作了.
而事实上学习的目的最终也是想设计并制作出这样一块电路板而已.

总结:
比如当你拆开一台手机你至少会看到有块PCB主板,这块主板上面肯定有各种芯片和元器件,其中肯定有一块是SOC(假设是S3C2410),那么这块SOC里面肯包含了ARM920T的内核,而这内核又属于ARMv4T架构

关系如下:
PCB主板==包含==>SOC==包含==>ARM内核==属于==>ARM架构

不同的ARM书籍侧重的层次都不尽相同,所以事先一定要了解清楚到底是想学什么.


C语言中的随机数

今天有空看了一下C语言(以VC6为具体实现平台)中的随机数的实现,我们先看一个例子:
程序代码:[ 复制代码到剪贴板 ]
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
    int i;
    srand(time(NULL));
    for(i=0; i<RAND_MAX; i++)
    {
        printf("%d\n", rand());
    }
    return 0;
}

其中RAND_MAX是一个宏, 值等于 0x7fff, 十进制的32767,这个值是C语言中short类型的最大值. srand是初始化随机数种子(seed)的函数,是以当前时间(time(NULL))为参数, 那随机数种子是什么东西, 为什么要以当前时间为参数, 带着这些问题我们再看一个例子:
程序代码:[ 复制代码到剪贴板 ]
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int i;
    /************************************************************************/
    /* 你会发现第一行的5个数和第二行的5个数是一样的,但是他们的顺序是反的.   */
    /************************************************************************/
    printf("第一部分: \n\n");
    for(i=0; i<5; i++)
    {
        srand(i);    //种子从0递增到4
        printf("%d   ", rand());
    }
    printf("\n==========================\n");
    for(i=0; i<5; i++)
    {
        srand(4-i); //种子从4递减到0
        printf("%d   ", rand());
    }

    /************************************************************************/
    /* 你会发现第二部分的输出结果和第三部分的输出结果是一模一样             */
    /************************************************************************/

    printf("\n\n第二部分: \n");
    srand(512);
    for(i=0; i<5; i++)
    {
        printf("%d\n", rand());
    }

    printf("\n第三部分: \n");
    srand(512);
    for(i=0; i<5; i++)
    {
        printf("%d\n", rand());
    }

    printf("\n");
    return 0;
}

这个例子的输出结果如图:

按此在新窗口打开图片

由此可见, 利用不同的随机数种子产生的随机数是不同的, 而使用相同的随机数种子所产生的随机数序列都是一模一样的. 那既然是随机数,为什么产生的结果又会是一样, 带着这个疑问我们再看看VC6中随机数相关的2个标准库函数的实现:
程序代码:[ 复制代码到剪贴板 ]
#ifndef _MT
static long holdrand = 1L;
#endif  /* _MT */

void __cdecl srand (
        unsigned int seed
        )
{
#ifdef _MT /*多线程支持*/

        _getptd()->_holdrand = (unsigned long)seed;

#else  /* _MT */
        holdrand = (long)seed;
#endif  /* _MT */
}

int __cdecl rand (
        void
        )
{
#ifdef _MT /*多线程支持*/

        _ptiddata ptd = _getptd();

        return( ((ptd->_holdrand = ptd->_holdrand * 214013L
            + 2531011L) >> 16) & 0x7fff );

#else  /* _MT */
        return(((holdrand = holdrand * 214013L + 2531011L) >> 16) & 0x7fff);
#endif  /* _MT */
}

看了后发现原来rand 函数的实现是基于线性同余法(Linear Congruential Method, LCM) ,主要是利用了如下公式: 
Xi+1=(a*Xi+c)mod m;
其中a为乘子(常数),C为增量(常数),X0(i=0时)为种子,m为模. 
线性同余法有如下特点:
(1)0≤Xi≤m-1,即Xi只能从0,1,2,……,m-1这m个整数中取值;
(2)适当选择m,a,c,可使Xi产生循环,无论X0取何值,其循环顺序是相同的.其循环周期称为发生器周期,记为P.若P=m,则称该发生器具有满周期. 该类型的函数也被称为线性适配函数(linear congruential function).

利用这种方法可以产生一个周期比较长的各不相同的数所组成的数列,所以在一定的范围里可看成是随机数列. 但是它并不是真正的随机数, 所以一般称之为伪随机数, 可以把它理解为有规律的随机数(感觉很矛盾 ), 而控制这种规律性的最关键的要素就是随机数种子, 所以我们用当前时间做为种子,因为时间是一直在变动的. 那为什么不产生真正的随机数呢,那是因为计算机产生真正随机数方法复杂,实现麻烦,所以C语言用某种数学递推公式来产生伪随机数, 来达到与真正随机数相似的效果. 下面是一个随机数使用例子.
程序代码:[ 复制代码到剪贴板 ]
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>

//产生一个区间[iStart,iEnd]上的随机浮点数
inline double RandomRange(int iStart, int iEnd)
{
    return ((double)rand()/RAND_MAX)*(iEnd-iStart) + iStart;
}

//这个函数有75%的可能性返回TRUE, 有25%的可能性返回FALSE
inline BOOL ProbabilityTest()
{
    return (rand()&3);
}

int main()
{
    int i;

    /*******************************************************/
    /* 第一部分 */
    /*******************************************************/
    srand(time(NULL));
    for (i=0; i<RAND_MAX; i++)
    {
        int iResult = rand() % 101;    //产生一个从1到100的随机数
        printf("%d\n", iResult);
    }

    /*******************************************************/
    /* 第二部分 */
    /*******************************************************/
    Sleep(1000);

       srand(time(NULL));
    for (i=0; i<RAND_MAX; i++)
    {
        double dbResult = RandomRange(5, 6); //产生一个大小介于5~6之间的浮点数
        printf("%f\n", dbResult);
    }

    /*******************************************************/
    /* 第三部分 */
    /*******************************************************/
    Sleep(1000);

       srand(time(NULL));
    int A = 0;
    int B = 0;
    for (i=0; i<RAND_MAX; i++)
    {
        if (ProbabilityTest())
        {
            A++;
            printf("有75%的可能性被执行\n");
        }
        else
        {
            B++;
            printf("有25%的可能性被执行\n");
        }
    }

    printf("A: %f   B: %f\n", (double)A/RAND_MAX, (double)B/RAND_MAX);

    return 0;
}


上面所提到的随机数生成器已经能够满足一般的需要了,但是却谈不上完美,事实上它本身还有很多缺点,首先它依赖于种子,它是利用种子计算出随机数,看了代码就知道,上一个随机数是下一个随机数的种子,本身就有很强的规律性,所以很容易遭受攻击,下面就是两起典型的案例:
一. Netscape Navigator浏览器早期版本的攻击可能是最著名的可预测随机攻击,其中用于其SSL(Secure Sockets Layer,安全套接字层)密钥的随机数有着很高的可预测性,使得SSL 失去意义.
二. 另一个就是攻击ASF软件公司的TexasHoldem Poker应用程序.这种'发牌'软件在算法中利用了Borland Delphi的随机函数.这个随机函数的实现类似于上面的rand()函数.
    正是因为它有这些缺点,所以大家把此类随机数称为伪随机数,这个'伪'字就带有'规律性'的意思. 那么到底有没有'真正的随机数'呢? 熟悉linux的人就知道,linux内核提供了一个叫/dev/random的接口,通过读取这个接口就可以获取'真正的随机数'. 那它是怎么实现的呢,在内部其实linux维护了一种称为"熵"(entropy)的东西.
那什么是"熵"? 你可以把"熵"理解成一系列的随机因子, 比如用户按键,定时器芯片的误差,空闲时间,中断时间等偶然因素. 正是利用了这些偶然的因素才能产生所谓的'真正的随机数'.  那在windows下面有没有类似的方法?当然有.  windows中提供一个API: CryptGenRandom(), 申明于Wincrypt.h. 下面是这个函数使用的一个例子:
程序代码:[ 复制代码到剪贴板 ]
#include <windows.h>
#define _WIN32_WINNT 0x0400
#include <wincrypt.h>


HCRYPTPROV hCryptProv; 

BOOL random_init()
{
    return CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0); 
}

ULONGLONG random()
{
    ULONGLONG uResult;
    if (!CryptGenRandom(hCryptProv, sizeof(uResult), reinterpret_cast<LPBYTE>(&uResult))) 
        return 0;
    return uResult;
}

void random_end()
{
    if(hCryptProv != NULL) CryptReleaseContext(hCryptProv, 0); 
}

int main(int argc, char* argv[])
{
    int nRetCode = 0;
    if (random_init())
    {
        for (int i=0; i<=1000; i++)
        {
            printf("%I64u\n", random());
        }
        random_end();
    }
    return 0;
}

    随机数生成本身就是一个很深奥的课题, 本文仅仅是粗略的介绍了一下,希望能对新手有所帮助. 


普通程序员的角度来看密码学

说起密码,说起密码学,第一时间想到的就是各种非常复杂的数学模型,种类繁多的各种标准,似懂非懂的各种专业术语.我相信有相当一部分程序员有这样的感受,我就是典型的代表.我不是数学特别好的那种,读书的时候也没专门学过密码学,所以当后来做与之相关的工作的时候(比如通信安全)总是不得要领,于是决定好好啃啃这块硬骨头,好在努力没白费,在学习和实践中慢慢的对密码学的应用有了些了解,决定把一些心得写出来,给后来的一些新手一点点帮助, ,不敢谈什么数学原理,只是站在一个普通程序员的应用角度来谈谈心得.
先看看密码学对于计算机实际应用到底有什么帮助,至少包括以下4个方面:

一. 保密性
   保密性是指隐藏信息所要表达的真实含义和目的.比如A有个很重要的文件,想通过网络传给B,但是他又不想让B以外的任何其他人知道,那么他首先和B约定好一个密钥,然后在通过网络传输前先把文件内容通过某种算法用约定好的密钥加密,将明文变成密文,然后再传给B,B接收到文件后再用约定好的密钥解密,将密文变成明文.在网络传输过程中就算被其他人窃取,窃取到的也是加密后的文件.从而达到保密的效果.有一种最常见的加密算法称之为AES算法.

二. 完整性
   完整性是指(假定数据内容不会被攻击者非法修改的前提下)确保数据和信息的正确性.比如A有个文件,在传给B之前先通过散列算法算出一个散列值,然后把文件和散列值一起传给B,B收到文件后通过同样的散列算法算出这个文件的散列值,然后和A发过来的散列值做比较,如果一样就证明这个文件是完整的,没被修改. 讲到这里,我想强调的是也许大家注意到了前面提到的一个前提条件(括号中的内容)会很奇怪,呵呵,关于这点后面会详细解释.有几种比较常见的散列算法,比如MD5算法,SHA-1算法.

三. 认证
   举个例子.比如A有个重要信息,想通过网络传给B,假设在传输的过程中可能会被黑客攻击,修改内容,甚至将内容完全替换,那么B要如何才能确认这个重要信息是不是正确?是不是A发过来的呢? 我们采取这样一种方法: 首先A和B要商量好一个密钥, 然后A在发送信息前首先将要发送的重要信息和密钥通过某种算法计算出一串值,这串值称为消息标记,然后把重要信息和计算出来的消息标记一起发给B,然后B将收到的重要信息和密钥通过同样的算法算出消息标记,然后和收到的消息标记做比较,如果相同就意味着数据正确并且完整,同时也能够确认这个信息确实是A发过来的,从而达到认证的目的.如果不相同就意味着可能被黑客给攻击了. 刚才使用的那个算法,我们通常称为 消息认证码(Message Authentication Code,MAC).看到这里,大家心里是不是会有一个疑问,这里提到的方法也可以验证数据的完整性,那么这里与上面所讲的 第二点:完整性 又有什么联系和区别呢? 呵呵 等一下会详细解释这个问题.

四. 不可否认
   什么叫不可否认?举个现实中的例子,比如你和人家签了份合同,当你在合同上签了字后那么这个合同就产生了法律效应,以后你做事就不能违背合同上的规定,你也不能否认你没有签过这个合同,因为合同上有你的签名. 那么在计算机的世界要如何达到这个目的呢?我们可以采取数字签名技术来达到这个目标,那么到底什么又是数字签名呢?文章下面会讲到 .

    前面说了密码学在计算机应用上的四个方面,下面就大概介绍一下密码学中的一些常用算法及其使用.
首先要谈到的是 对称密钥算法  所谓对称的含义是指一个加密算法的加密密钥和解密密钥相同,或者虽然不相同,但是可由其中的任意一个很容易的推导出另一个,即密钥是双方共享的. 这种类型的算法的典型代表是AES(Advanced Encryption Standard)高级加密标准算法,也叫Rijndael算法.它是一种分组密码(block cipher)算法.它是美国国家标准与技术协会(NIST)所认可的一个标准.AES算法的出现是用来替代一个古老的算法:DES(Data Encryption Standar)数据加密标准算法的.DES算法已经被多次证明是不安全的!

按此在新窗口打开图片

   不过这种类型的算法都有个明显的不足, 就是它的密钥是共享的, 只要有一个人泄露了密码,那么整个体系就都不安全了.

    接下来我们要谈到的是散列函数,散列函数也称为(Pseudo Random Function, PRF)伪随机函数.散列函数是指的这样一种算法: 它可以把一个任意大小的输入数据通过一种压缩(compression)的处理过程转换为一个固定大小的不会重复(极少重复)的输出.而且这种压缩处理是不可逆的,意味着你没有办法将输出还原成输入.这种算法的输出有个专业称呼,称之为摘要(digest) 如图:

按此在新窗口打开图片

   比较常见的散列算法有MD5,SHA-1等.先看看MD5,我想这个大家应该太熟悉不过了,MD5的全称是(message-digest algorithm 5)信息-摘要算法,经MD2、MD3和MD4发展而来,MD5算法能够将任意长度的输入数据映射成一个256位(32个字节)长的输出(摘要). 不过MD5算法现在已经被证明是不安全的了. 再看看SHA-1算法,它是SHS(Secure Hash Standard)安全散列标准家族中的一员,而SHS又隶属于美国国家标准与技术协会(NIST).SHA-1能够将任意长度的输入数据映射成一个160位(20个字节)长的输出(摘要). 下面我们来看看散列算法如何来保证数据的完整性,举个实际的例子:BT下载软件大家想必都用过,它首先需要一个种子文件,然后打开这个种子文件才能下载到实际的数据,其实这个种子文件中就包含了要下载的数据的摘要信息,这个摘要是通过SHA-1算法算出来的,当你打开种子文件的时候会通过SHA-1算法计算出已经下载过的数据的数据摘要,和种子文件中纪录的摘要做比较,如果全部相同就证明数据已经全部下完.如果不同就意味着数据还没下完或者是被非法修改过. 
    不过通过散列函数保证数据完整性有时候确会碰上麻烦,不信我们看个例子:A想把一个重要信息通过网络传给B,A在传送之前首先通过散列算法比如SHA-1算法算出这个信息的摘要,然后把摘要附加到信息的尾部一起传送给B,假设这个时候黑客C在传送途中把数据包给拦截下来并且把信息内容进行修改,然后重新用SHA-1算法算出修改后的信息摘要替换原来的摘要,再转发给B,这个时候B收到了信息,B用SHA-1校验信息后发现一切OK,他以为他收到了正确的信息数据,殊不知这个重要信息早已经在网络传输的途中被C给改掉了. 如图:

按此在新窗口打开图片

   可见散列算法虽然能保证数据完整性,但是却有个前提条件,正如文章前面说讲的一样,(假定数据内容不会被攻击者非法修改的前提下), 大家是不是觉得很矛盾, 但事实确实是这样 . 通过前面这个例子可以看出: 散列算法虽然能够对数据完整性提供保证,但是却无法对数据的来源进行认证(B没有办法确认文件就是A给他发送的那个). 那么如果想达到认证的目的要怎么办了,文章前面谈到过MAC算法,那么什么是MAC算法,我们接着看.

    消息认证码(Message Authentication Code,MAC)算法存在的主要目的就是为了认证, 美国国家标准与技术协会(NIST)制定二种MAC标准.
第一种是分组消息认证码(Cipher Message Authentication Code, CMAC)算法. 
第二种散列消息认证码(Hash Message Authentication Code, HMAC)算法.
    CMAC算法是基于分组密码算法的,很多情况下CMAC算法内部使用了AES算法. 而HMAC算法是基于散列算法的,很多情况下HMAC算法内部使用了SHA-1算法. 不管是哪种MAC算法 它们的外部表现形式都是一样的: 它们都接受一个任意大小的输入和一个密钥输入,然后通过计算得出一个固定大小的输出,我们一般把这个输出称为MAC标记.如图:

按此在新窗口打开图片

   我们来看例子: A想把一个重要信息通过网络传给B,首先A和B要先约定好一个密钥,A在传送之前首先利用密钥通过MAC算法算出这个信息的MAC标记,然后把MAC标记附加到信息的尾部一起传送给B,假设这个时候黑客C在传送途中把数据包给拦截下来并且也把信息内容进行了修改,但是他因为不知道密钥,所以他无法重新计算MAC标记,所以攻击失败. 当B收到信息以后,他用约定好的密钥通过MAC算法算出收到的信息的MAC标记和收到的MAC标记做比较, 如果一样就意味着数据是A发来的并且完好无损. 把这个例子和上面讲散列算法的那个例子做比较, 会发现MAC算法比散列算法多了一个输入参数, 那就是密钥 , 正是因为多出的这个密钥参数才保证了MAC算法能够达到认证的目的, 当然同时也能够实现完整性验证.

    前面讲了MAC算法,你会发现它只是提供了完整性和认证的功能, 却没有提供对数据的内容进行加密的功能. 所以实际的应用中很可能要和某种加密算法配合使用,比如我传输数据之前首先用AES算法对数据加密,然后用MAC算法实现数据的认证.  那有什么办法可以把加密和认证的功能合到一起,至少我们写程序的时候可以一步到位 . 答案是有的, 接下来就介绍.

    加密和认证模式(encrypt and authenticate modes) 指的是把加密和认证的任务封装到一个单独的处理过程中,下面是一个非常简化的过程图:

按此在新窗口打开图片

其中最常见的二个标准: 
一个是美国电气及电子工程师学会 IEEE(Institute of Electrical and Electronics Engineers)的GCM(Galois Counter Mode), 它常用于各种无线标准,比如802.16. 
另一个是 NIST的CCM(Counter mode with Cipher-block chaining Message authentication code).

    其实不管是加密也好认证也罢,有个问题都无法直接解决,这个问题就是重放保护问题.那什么是重放保护呢? 举个例子,看看如果没有重放保护会发生什么大问题. 比如我使用网上银行转帐,在这个过程中, 网上银行客户端程序向银行服务器发送了一个数据包,这个数据包里面包含了转帐的请求和金额,当服务器收到这个数据包就会根据请求进行转帐操作, 假如在通讯过程中这个数据包被黑客给拦截了,虽然黑客没办法把数据包解密或是修改,但是他可以把这个数据包保存下来,然后把数据包原封不动的不停的给银行服务器发,那最终我账户上的资金就会被全部转走 , 那可就惨啦! 当然这仅仅只是个假设,但是它反应出重放保护是多么的重要! 为了做到重放保护我们必须做点额外的工作,最常用的方法就是在通讯数据包中加入时间戳或者是计数器. 如果包含了时间戳,接收到数据包后可以对比时间差,如果时间间隔太久就丢弃数据包. 如果包含了计数器,接收到数据包后可以查看包的编号,比如收到的第一个包编号是2, 那么收到的第二个包的编号至少比2要大.不然就丢弃数据包.这样就可以比较好的解决重放保护问题.

    上面介绍的不管是AES算法还是MAC算法它们都是对称算法, 通讯双方的密钥是共享. 所以一但任何一方泄露了密钥, 就再没有安全可言.  而非对称密钥算法就能很好的解决这个问题.

    非对称密钥(asymmetric key)算法,也叫做公钥算法. 它的出现解决了对称加密算法很难解决的两个问题: 密钥分发(key distribution) 和 不可否认(nonrepudiation). 而RSA(Rivest Shamir Adleman)算法是一种最常见的公钥算法. 公钥算法的加密和解密所使用的密钥是不同的,其中公钥是可以公开的,而私钥必须保密.用公钥加的密必须用对应的私钥才能解密,用私钥加的密必须用对应的公钥才能解密. 而且无法由公钥推导出私钥.
    先看看公钥算法如何解决密钥分发的问题.举个例子: A要通过网络传一个文件给B,那A首先问B要一个公钥,B自己持有相应的私钥,然后A用这个公钥加密另一个用于对称加密算法的密钥(用K表示),然后把加了密的密钥发给B,B收到后用自己的私钥解密,得到用于对称加密算法的密钥(K),这个时候A和B就都知道了用于对称加密算法的这个密钥(K),然后A把文件用对称加密算法(比如AES)加密 传给B, B收到后用密钥(K)将收到的文件解密. 在整个过程中,就算黑客拦截了传输密钥(K)的数据包,但是他没有私钥无法解密,所以他不知道密钥(K)是什么,所以他也无法对传输的文件进行解密. 

按此在新窗口打开图片

    大家会发现上面的例子中是混合使用了对称密钥算法非对称密钥算法,这种混合模式在实际的应用当中经常被采用. 那为什么不直接采用非对称密钥算法完成所有的工作呢? 那是因为一般而言,非对称密钥算法加密的速度都比对称密钥算法的多,所以采用这种混合模式可以充分发挥对称密钥算法的速度优势.
    前面在说不可否认那节时说到了数字签名,那什么是数字签名呢? 所谓数字签名就是附加在数据单元上的一些验证数据.接收者利用这些数据确认数据单元的来源和数据单元的正确性并防止别人伪造. 数字签名一般是利用散列算法和公钥算法来完成的. 如图:

按此在新窗口打开图片

    把将要签名的数据用散列函数算出散列值(摘要),再把散列值(摘要)传给公钥算法,最后算出的结果就是这个数据的数字签名.  举个例子: A有一个文件要传给B,A用SHA-1算法算出文件的摘要,再把摘要用自己的私钥通过RSA算法加密,得出数字签名后连同文件本身一起发给B, B收到后,首先用A的公钥通过RSA算法将数字签名解密,解出摘要.然后他把收到的文件也通过SHA-1算法算出摘要, 然后把解密出来的摘要和自己算出的摘要做比较,如果相同就意味着数字签名验证通过. 这样就可以确认文件是完整的而且确实是A发过来的,A也无法抵赖说文件不是他的,因为B是用A的公钥解密的. 

    文章上面讲到了几种典型的算法和几种典型的应用,在实际应用当中可以根据自己项目的需要自由搭配各种加密算法,组合出最适合自己项目的一种安全解决方案. 还有就是在实际应用中, PKI(Public Key Infrastructure)公钥基础设施,是一种遵循既定标准的密钥管理平台.应用范围非常的广,值得我们大家一起学习. http://www.infosecurity.org.cn 这个网站可以看看. 像<<现代密码学>>, <<程序员密码学>>,<<密码学导论>>等书也可以看看. 下面是开源社区的几个加密算法库,比较著名. 可以在自己的项目中使用. 研究这些代码也是学习的好途径.

OpenSSL     http://www.openssl.org 
LIBTOM       http://www.libtom.org 
Crypto++     http://www.cryptopp.com 


被应用于分布式软件设计的经济学难题:囚徒困境

BT软件讲究的是人人为我, 我为人人, 讲究的是上传的越多, 那自己下载的也越顺利, 但是在实际的操作过程中, 却会碰到一个很有趣的问题: 囚徒困境, 这本就是个经济学难题, 它讲叙的是下面这样一个故事.

甲和乙一起犯了罪被抓。他们分别被告知:如果都不招,则判1年;如果一个招,则招的人释放,另一个判10年;如果都招了,则各判5年。于是甲就想:如果乙不招,那么我招,可以不判刑;如果乙招了,那么我招,可以少判5年;不管怎样,招供都是最佳选择。而乙恰恰也是这么想的。最后两人都被判5年。 

显然两人都作出了最明智的选择,最后却得到了一个不是众乐乐(应是各判1年)的局面。囚徒困境告诉我们:最符合个体理性的选择,却是集体非理性的。


发现VC8.0的一个BUG

前段日子在用VC8.0调试程序的过程中发现了它的一BUG, 今天有空决定把它写出来. 看看下面这段代码:
程序代码:[ 复制代码到剪贴板 ]
    int A = 255789;
    float B = (float)A/(float)100;
    float C = B * 100;
    printf("result: %f \n\n", C);

在VC6.0下不管是Debug方式编译,还是Release方式编译 执行结果都正确.  如图:

按此在新窗口打开图片

然而在VC8.0下不管是Debug方式编译,还是Release方式编译 执行结果都有点怪. 如图:

按此在新窗口打开图片

装了VS80sp1的补丁后问题依旧,  比较了一下发现问题就出现在第二条语句float B = (float)A/(float)100; 
看了看生成的汇编码, 下面是vc6 Debug编译的

程序代码:[ 复制代码到剪贴板 ]
__real@4@4005c800000000000000 dd 1.0e2

//int A = 255789;
mov     [ebp+A], 255789

//float B = (float)A/(float)100;
fild    [ebp+A]
fdiv    ds:__real@4@4005c800000000000000 //操作的数是DWORD型, 定义在上面
fst     [ebp+B]


下面是vc8 Debug编译的

程序代码:[ 复制代码到剪贴板 ]
__real@4059000000000000 dq 1.0e2 

//int A = 255789;
mov     [ebp+A], 255789

//float B = (float)A/(float)100;
fild    [ebp+A]
fdiv    ds:__real@4059000000000000  //操作的数是QWORD型, 定义在上面
fstp    [ebp+B]


   众所周知,  IA32 CPU的浮点运算是基于栈的,  浮点单元包括8个浮点寄存器,  每个浮点寄存器的位宽都是80位,  和普通寄存器不同的是, 它们被当成一个浅栈(shallow stack)来对待,  这些寄存器分别标识为st0,st1~~直到st7. 其中st0在栈顶. 当压入栈中的值超过8个时,  栈底的值就会消失. (具体请参看<<深入理解计算机系统>>). 浅显点来讲就是利用某些浮点指令把被操作的数PUSH到浮点栈中,等CPU运算完成后再利用某些浮点指令把结果从栈中POP出来.  扯远了, ,  继续看我们的问题, 比较上面2段汇编代码就会发现他们有2处不同:
   第一处就是fdiv指令, 在VC6中它操作的是DWORD型的数, 在VC8中它操作的是QWORD(QWORD大小为8个字节), 在VC中类型float代表单精度浮点数, 占用大小为4个字节也就是32位, double类型代表双精度浮点数, 占用大小为8个字节也就是64位. 通过我的测试发现在VC8中 (1)float B = (float)A/(float)100; (2)float B = (float)A/(double)100; 这2条语句用VC8编译后居然没有任何区别, 内存中100都是用64位的双精度浮点数表示的, 而同样的语句用VC6编译的话 那么语句(1)100就会用32位单精度浮点数表示, 而语句(2)就会用64位的双精度浮点数表示, 我认为这应该是VC8的一个BUG. 
   第二处不同就是 fst,fstp的区别, 先看看这2条指令的说明.
FST
 指令格式:FST  STReg/MemReal
 指令的功能:将协处理器堆栈栈顶的数据传送到目标操作数中。在进行数据传送时,系统自动根据控制寄存器中舍入控制位的设置把栈顶浮点数舍入成相应精度的数据。
FSTP
 指令格式:FSTP  STReg/MemReal
 该指令的功能与FST相类似,所不同的是:指令FST执行完后,不进行堆栈的弹出操作,即:堆栈不发生变化,而指令FSTP执行完后,则需要进行堆栈的弹出操作,堆栈将发生变化。

 经过我的测试, 这个地方的不同才是导致运算结果有区别的直接原因, 哪怕把前面那2段汇编代码改成一样,只保留FST和FSTP的不同, 执行后还是会产生不同的结果, 不知为何. 各位有时间也可以自己测试一下, 知道原因了一定要告诉我. 我这里有个浮点指令体系的说明, 是以前网上下的, 觉的写的也还浅显易懂就收集了, 这里把它做为附件也一并附上  

点击下载此文件


错误 C1076

前段日子在windows平台下开发了一BT软件,其中用到了libtorrent这个开源的BT库,当时用的版本是0.12版,
好久没去看发现libtorrent库已经升级到了0.13版,看了看发布报告,发现这个版本比0.12版解决了很多BUG,并且增加了很多新功能,所以决定试一试,把自己使用的版本也更新到0.13版,说弄就弄,做了一系列工作后,开始编译,居然报了个C1076的错误,猜想可能是0.13版使用了更多的模版代码所引起的

按此在新窗口打开图片

查了查资料:

致命错误 C1076
错误消息 (编译器限制 : 达到内部堆限制;使用 /Zm 指定更高的限制)
此错误可能是由过多符号或过多模板实例化引起的。
解决此问题的方法是:
使用 /Zm 选项设置编译器内存限制。
消除不需要的包含文件。
消除不需要的全局变量,例如,动态分配内存而不是声明一个大数组。
消除未使用的声明。
将大函数拆分为更小的函数。
将大类拆分为更小的类。
将当前文件拆分成更小的文件。
如果在生成开始后立即发生 C1076,则说明为 /Zm 指定的值对程序而言可能太高。请减小 /Zm 的值。

呵呵,果然如我猜想的那样. 在编译指令当中多加了条/Zm200, 随即解决问题.


软件开发知识学习分类之我见

我把软件开发要学习的知识分为4大类:

一:计算机语言(包括编译原理啊,语言模型啊,比如学习C++语法啊,面向对象啊,范型编程啊,标准库啊,等等)

二:操作系统和硬件知识(这个是最大,最杂的一个分类,比如操作系统就有windows,linux,unix啊,硬件设备也是各种各样,操作系统又有应用层,核心层之分啊,每个层次上又可以分为很多小类别,比如说多媒体编程啊,网络编程啊等等,单单一个网络编程又可以细分为太多的小类啦,比如windows网络驱动模型中,又可以分为NDIS协议层驱动,NDIS中间层驱动,NDIS小端口驱动等,具体我就不多说啦)

三:数据库(把数据库单独出来我想没人会反对吧,数据库一般都是以一个应用软件实体存在的,但是它包含的内涵完全就是一个独立的学科,现在的数据库产品也是品种繁多,比如Microsoft公司的SQL Server, 甲骨文的Oracle数据库, IBM的DB2, 开源的MySQL, 面向嵌入式领域的SQLite等等,虽然说有这么多不同的实现版本,但是它们的基本原理却都是相通的,我对数据库方面研究不深,所以就不多说啦)

四:软件工程(这个更多是偏向管理方面,因为当软件越做越大,开发人员越来越多时,管理问题就不可避免,所以软件工程不可不学,它包括各种工具软件的使用,如BUG管理,单元测试,设计与建模,开发环境,各种类库及其文档,版本控制等等,还有各种软件开发方法,如敏捷开发(agile development), 测试驱动开发(Test Driven Development,英文缩写TDD)等, 将软件工程化就是它的目标,利用各种工具组成一套完整的软件生产线,而程序员在开发方法的指导下利用生产线高效的生产出各种软件产品,你说这是不是和一些传统工厂模式很像啊,比如一间造汽车的工厂.)


«1234567»

Powered By Z-Blog 2.2 Prism Build 140101

Copyright phonegap.me Rights Reserved.