HackWare Loader PC工作

前面的博文,差不多对 BP 所使用的 BL 做了个十分粗浅的理解了, 这一篇, 是对于其PC端上的和BootLoader 共同作用的一个软件(Pirate-loader)的一个源码的阅读笔记。对其原理进行学习。


文件定位 :Bootloaders/pirate-loader/pirate-loader.c

源码 Src

系统条件编译宏

这个标题可能是不大准确,实现的是我们很长用的功能,我们使用 GCC 编译器的时候, 在不同的平台编译,使用平台的特定系统API,实现功能相同的底层函数。(linux下的就是linuxC,Win下的就是Winapi)。

下面就是实现的预编译语句,在win下的编译过程中,编译器会自动的帮我们定义了 WIN32 这个宏。

#ifdef WIN32    #include <windows.h>    #include <time.h>    #define O_NOCTTY 0    #define O_NDELAY 0    #define B115200 115200    #define OS WINDOWS    ...#else    // unix/linux    #include <unistd.h>    #include <termios.h>    #include <sys/select.h>    #include <sys/types.h>    #include <sys/time.h>#endif#if !defined OS    #define OS UNKNOWN#endif

Win的函数封装

在这种多平台编译的情况下,统一接口就是比较重要的过程了,在这个工程里,原作者使用win函数进行进一步封装,实现和 Linux 环境下的统一的接口,在后面的功能代码里面直接进行调用即可,这是个很好的思想,学习了


例如这里是,一个写函数的实现,

int write(int fd, const void* buf, int len)    {        HANDLE hCom = (HANDLE)fd;    // 这里的文件描述符实际上是句柄了        int res = 0;        unsigned long bwritten = 0;        res = WriteFile(hCom, buf, len, &bwritten, NULL);        if( res == FALSE ) {            return -1;        } else {            return bwritten;            // 已写入字节        }    }

我们直接使用 man 2 write # 查看系统接口,可以看到,这个write函数的定义原型prototype:

size_t    write(int fildes, const void *buf, size_t nbyte);

显然上面的定义是进行了相同的封装。

int read(int fd, void* buf, int len){    ...     // 和write类似}int close(int fd){    HANDLE hCom = (HANDLE)fd;    CloseHandle(hCom);        // 关闭句柄    return 0;}

再下面的,这个open就是比较重要的一个函数了。具体的实现过程:

int open(const char* path, unsigned long flags){    static char full_path[32] = {0};    // buf溢出风险    HANDLE hCom = NULL;    // 这里很是眼熟    if( path[0] != '\\' ) {        _snprintf(full_path, sizeof(full_path) - 1, "\\\\.\\%s", path);        path = full_path;    }    // 这里是打开串口的操作,后面的参数,OPEN_EXISTING, 说明了存在就打开    // 打开之后,返回我们的串口句柄    hCom = CreateFileA(path, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);    if( !hCom || hCom == INVALID_HANDLE_VALUE ) {        return -1;    } else {        return (int)hCom;    }}

实际上查看了相关的文章发现, 在win里面进行串口的打开实际上只是需要一个 ‘COMX’ 的端口号就是可以直接试一下 CreateFile 对串口进行打开。

不过这样前面的 _snprintf 的用法就是显得很迷了?为什么。突然一看后面, 有些眼熟 \\.\COMx 这个格式十分像之前的 win里面进程间通信的有名管道的用法。没错的。


也找到了这个写法的真正原因:

如果我们使用过 SMB 的服务,我们会发现在,进行计算机链接的时候我们的键入内容是?

\\192.168.x.x

这样就是表面了对远程主机是发起了连接。转回到这里
\.\COMx
说明了什么? 连接到 . 主机(也就是本地主机)的COMx, 妙哉。


轮询

int __stdcall select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfs, const struct timeval* timeout){        // 当前时间加上轮询时间    time_t maxtc = time(0) + (timeout->tv_sec);    COMSTAT cs = {0};    unsigned long dwErrors = 0;    if( readfds->fd_count != 1 ) {        return -1;    }    // 条件判断,在轮询时间内进行串口轮询    while( time(0) <= maxtc )    { //only one file supported        if( ClearCommError( (HANDLE)readfds->fd_array[0], 0, &cs) != TRUE ){            // 失败就是直接返回            return -1;        }        if( cs.cbInQue > 0 ) {            // 成功这里就返回            return 1;        }        Sleep(10);    }    return 0;}

Select 函数,在linux 下实现的是一个 I/O的复用,准确说是时分复用,这样相当于是给了内核任务,去查看IO的状态。这样就实现了重要的一点, 就是非阻塞IO。所以在Select的精妙的作用下, 可以实现在单线程里面,实现多个IO。

具体的使用,是根据其返回值,判断当前IO的状态,然后我们可以遍历 描述符集合。

  • Linux 下的Select函数

关于 __stdcall 的调用规则修饰,这里再加强记忆一下。

  • 函数的调用规则(cdecl,stdcall,fastcall,pascal)

这种调用规则,是从右到左的参数压入顺序,被调用者把参数弹出栈, 用在winapi 里面是比较多的。其函数输出符号是 _func@12 后面是参数的字节数。

IO函数 封装

上面的部分已经实现了对 Win和l下的接口的统一封装。

这一步是对一些IO细节的封装,以便适应我们的应用。


读函数
由其定义名称得知,是带有IO超时的读函数。

int readWithTimeout(int fd, uint8* out, int length, int timeout){    fd_set fds;    // 这里是select使用的结构    struct timeval tv = {timeout, 0};    int res = -1;    int got = 0;    do {        // 这里是对 文件描述符的集合进行刷新            // 在select 里面会有关闭        FD_ZERO(&fds);        FD_SET(fd, &fds);        res = select(fd + 1, &fds, NULL, NULL, &tv);        if( res > 0 ) {            // 说明IO就绪            res = read(fd, out, length);            if( res > 0 ) {                length -= res;                got    += res;                out    += res;            } else {                break;            }        } else {             return res;        }    } while( length > 0);    // 这里是读到缓冲区长度    // 感觉有问题,这个可以超出out的长度    return got;}

该函数,实现了对串口状态的非阻塞轮询读,在串口可读时,就多次读取串口数据,进入缓冲区。不可读时,就进行返回。

这里的问题是,在缓冲区将满的时候,还是存在一个读操作,虽说只是读length个长度,这个长度也是在不断减少,这样讲,好像没什么问题了。


这个也算是个对底层IO的封装了吧。发送命令等待回应,这个函数调用了上面的读函数。

int sendCommandAndWaitForResponse(int fd, uint8 *command){    // 读取返回的状态字    uint8  response[4] = {0};    int    res = 0;    // 很正常的写操作    // 这里的长度值得分享    res = write(fd, command, HEADER_LENGTH + command[LENGTH_OFFSET]);    /fail    if( res <= 0 ) {        puts("ERROR");        return -1;    }    // 完成写之后,对串口进行读    res = readWithTimeout(fd, response, 1, 5);    if( res != 1 ) {        puts("ERROR");        return -1;    } else if ( response[0] != BOOTLOADER_OK ) {        // 串口返回的状态字,可以由PC解析发现什么问题        printf("ERROR [%02x]\n", response[0]);        return -1;    } else {        return 0;    }}

这里的前面的写操作所对应的,第三个参数是写入长度,可是这里加上的便宜,还涉及到了Cmd的内容

res = write(fd, command, HEADER_LENGTH + command[LENGTH_OFFSET]);

文件读取

这个算是关键函数,实现了对我们的固件的hex文件的读取

函数原型如下:

int readHEX(const char* file, uint8* bout, unsigned long max_length, uint8* pages_used)

篇幅很长这里对函数体的重点:

//////////////////////////////////static const uint32 HEX_DATA_OFFSET = 4;uint8* data = (linebin + HEX_DATA_OFFSET);char  line[512] = {0}; // 行指针组char  *pc;    // 字符指针char  *pline = line + 1;    // 行指针///////////////////////////////////// 这里的feof是标准库哦,学习了// 加载一行的内容while( !feof(fp) && fgets(line, sizeof(line) - 1, fp) )    {        // 行号,看得出来        line_no++;        // hex文件格式,        if( line[0] != ':' ) {            break;        }        // pline 是行内容指针        res = strlen(pline);        // 当前的行地址,加上行长度,减一,        // 也就是行末。        pc  = pline + res - 1;        // 注意, 这里的 <= ' '         // 一开始没有理解,发现空格的ascii是32,所以这里是去掉特殊字符,把他们直接给置0        while( pc > pline && *pc <= ' ' ) {            *pc-- = 0;    // 这里写0干嘛呢            res--;        }        // res 是当前行的字符剩余数,一下的情况都是非法的。        if( res & 0x01 || res > 512 || res < 10) {            fprintf(stderr, "Incorrect number of characters on line %d:%d\n", line_no, res);            return -1;        }        // CRC 校验        hex_crc = 0;                    for( pc = pline, i = 0; i<res; i+=2, pc+=2 ) {            linebin[i >> 1] = hexdec(pc);            hex_crc += linebin[i >> 1];        }        binlen = res / 2;        if( hex_crc != 0 )            ... // checksum失败                        ...                    if( binlen - (1 + 2 + 1 + hex_len + 1) != 0 )                ... // 字节数失败        if( hex_type == 0x00 )        {            f_addr  = (hex_base_addr | (hex_addr)) / 2; //PCU            if( hex_len % 4 ) {                // 数据没对齐 4字节                fprintf(stderr, "Misaligned data, line %d\n", line_no);                return -1;            } else if( f_addr >= PIC_FLASHSIZE ) {                // 编程地址超出PIC的flash地址                fprintf(stderr, "Current record address is higher than maximum allowed, line %d\n", line_no);                return -1;            }            hex_words = hex_len  / 4;            o_addr  = (f_addr / 2) * PIC_WORD_SIZE; //BYTES            for( i=0; i<hex_words; i++)            {                bout[o_addr + 0] = data[(i*4) + 2];                bout[o_addr + 1] = data[(i*4) + 0];                bout[o_addr + 2] = data[(i*4) + 1];                pages_used[ (o_addr / PIC_PAGE_SIZE) ] = 1;                o_addr    += PIC_WORD_SIZE;                num_words ++;            }        } else if ( hex_type == 0x04 && hex_len == 2) {            hex_base_addr = (linebin[4] << 24) | (linebin[5] << 16);        } else if ( hex_type == 0x01 ) {            break; //EOF        } else {            fprintf(stderr, "Unsupported record type %02x, line %d\n", hex_type, line_no);            return -1;        }    }    fclose(fp);    return num_words;}

从文件的 循环行读取正则 ,校验, 缓冲区偏移存储,记录值类型。完成了一个hex的读取的过程。

先是在循环中按行读取,根据前面的 hex_type 判断当前的读取的数据类型,当前行的实际字节数 hex_len 和地址,f_addr = (hex_base_addr | (hex_addr)) / 2 这一步,确定了,在flash的真实的地址映射,


!feof(FILE *stream)while( !feof(fp) && fgets(line, sizeof(line) - 1, fp) )

这个循环读取的结构, 很棒,学习了

发送固件

int sendFirmware(int fd, uint8* data, uint8* pages_used){    uint32 u_addr;    uint32 page  = 0;    uint32 done  = 0;    uint32 row   = 0;    uint8  command[256] = {0};    for( page=0; page<PIC_NUM_PAGES; page++)    {        u_addr = page * ( PIC_NUM_WORDS_IN_ROW * 2 * PIC_NUM_ROWS_IN_PAGE );        if( pages_used[page] != 1 ) {            if( g_verbose && u_addr < PIC_FLASHSIZE) {                fprintf(stdout, "Skipping page %ld [ %06lx ], not used\n", page, u_addr);            }            continue;        }        if( u_addr >= PIC_FLASHSIZE ) {            fprintf(stderr, "Address out of flash\n");            return -1;        }        //erase page        command[0] = (u_addr & 0x00FF0000) >> 16;        command[1] = (u_addr & 0x0000FF00) >>  8;        command[2] = (u_addr & 0x000000FF) >>  0;        command[COMMAND_OFFSET] = 0x01; //erase command        command[LENGTH_OFFSET ] = 0x01; //1 byte, CRC        command[PAYLOAD_OFFSET] = makeCrc(command, 5);        if( g_verbose ) {            dumpHex(command, HEADER_LENGTH + command[LENGTH_OFFSET]);        }        printf("Erasing page %ld, %04lx...", page, u_addr);        if( g_simulate == 0 && sendCommandAndWaitForResponse(fd, command) < 0 ) {            return -1;        }        puts("OK");        //write 8 rows        for( row = 0; row < PIC_NUM_ROWS_IN_PAGE; row ++, u_addr += (PIC_NUM_WORDS_IN_ROW * 2))        {            command[0] = (u_addr & 0x00FF0000) >> 16;            command[1] = (u_addr & 0x0000FF00) >>  8;            command[2] = (u_addr & 0x000000FF) >>  0;            command[COMMAND_OFFSET] = 0x02; //write command            command[LENGTH_OFFSET ] = PIC_ROW_SIZE + 0x01; //DATA_LENGTH + CRC            memcpy(&command[PAYLOAD_OFFSET], &data[PIC_ROW_ADDR(page, row)], PIC_ROW_SIZE);            command[PAYLOAD_OFFSET + PIC_ROW_SIZE] = makeCrc(command, HEADER_LENGTH + PIC_ROW_SIZE);            printf("Writing page %ld row %ld, %04lx...", page, row + page*PIC_NUM_ROWS_IN_PAGE, u_addr);            if( g_simulate == 0 && sendCommandAndWaitForResponse(fd, command) < 0 ) {                return -1;            }            puts("OK");            sleep(0);            if( g_verbose ) {                dumpHex(command, HEADER_LENGTH + command[LENGTH_OFFSET]);            }            done += PIC_ROW_SIZE;        }    }    return done;}

串口配置

打开串口之后,对串口的参数进行配置, 这部分代码可以收藏,重用机会是挺多的。

int configurePort(int fd, unsigned long baudrate){#ifdef WIN32    // 系统宏    DCB dcb = {0};    HANDLE hCom = (HANDLE)fd;    dcb.DCBlength = sizeof(dcb);    dcb.BaudRate = baudrate;    dcb.ByteSize = 8;    dcb.Parity = NOPARITY;    dcb.StopBits = ONESTOPBIT;    if( !SetCommState(hCom, &dcb) ){        return -1;    }    return (int)hCom;#else    struct termios g_new_tio;    memset(&g_new_tio, 0x00 , sizeof(g_new_tio));    cfmakeraw(&g_new_tio);    g_new_tio.c_cflag |=  (CS8 | CLOCAL | CREAD);    g_new_tio.c_cflag &= ~(PARENB | CSTOPB | CSIZE);    g_new_tio.c_oflag = 0;    g_new_tio.c_lflag = 0;    g_new_tio.c_cc[VTIME] = 0;    g_new_tio.c_cc[VMIN] = 1;    cfsetispeed (&g_new_tio, baudrate);    cfsetospeed (&g_new_tio, baudrate);    tcflush(fd, TCIOFLUSH);    return tcsetattr(fd, TCSANOW, &g_new_tio);#endif}

命令行解析

这里一样的是,一个挺实用的部分。也算是当做代码片收藏了

int parseCommandLine(int argc, const char** argv){    int i = 0;    // 从 1 开始解析参数,后面疯狂进行对比    for(i=1; i<argc; i++)    {        if( !strncmp(argv[i], "--hex=", 6) ) {            g_hexfile_path = argv[i] + 6;        } else if ( !strncmp(argv[i], "--dev=", 6) ) {            g_device_path = argv[i] + 6;        } else if ( !strcmp(argv[i], "--verbose") ) {            g_verbose = 1;        } else if ( !strcmp(argv[i], "--hello") ) {            g_hello_only = 1;        } else if ( !strcmp(argv[i], "--simulate") ) {            g_simulate = 1;        } else if ( !strcmp(argv[i], "--help") ) {            argc = 1; //that's not pretty, but it works :)            break;        } else {        // 没有找到对应的参数            fprintf(stderr, "Unknown parameter %s, please use pirate-loader --help for usage\n", argv[i]);            return -1;        }    }    if( argc == 1 )    {        //print usage        puts("pirate-loader usage:\n");        puts(" ./pirate-loader --dev=/path/to/device --hello");        puts(" ./pirate-loader --dev=/path/to/device --hex=/path/to/hexfile.hex [ --verbose ]");        puts(" ./pirate-loader --simulate --hex=/path/to/hexfile.hex [ --verbose ]");        puts("");        return 0;    }    return 1;}

虽说这里是很实用的 代码,不过感觉蠢蠢的,通过代码的遍历比较,感觉有什么不对,进行一个全局的标志位的操作。

不过也是很巧妙:

strncmp(argv[i], "--hex=", 6)        g_hexfile_path = argv[i] + 6;strncmp(argv[i], "--dev=", 6)g_device_path = argv[i] + 6;

突然一想,发现这个没有空格啊。

没错,是没有空格的,参数和这个输入的本身是没有空格的,使用的是 = 进行的连接,所以在后面使用 = argv[i] + 6; 这种形式,就可以直接偏移到我们的输入内容,妙哉

辅助函数

这部分就是一些辅助函数,进行字符转换之类的东西,虽说简单,但是写的精妙

// 这个函数,把十六进制字符串,转为整型值unsigned char hexdec(const char* pc){    unsigned char temp;    // 从ASCII从大到小,依次来    if(pc[0]>='a'){        temp=pc[0]-'a'+10;    }else if(pc[0] >= 'A'){        temp=pc[0]-'A'+10;            }else{        temp=pc[0] - '0';    }    // 第一个字符的整型值放在这个Char的高位    // 别忘了,Char可是8位的    temp=temp<<4;    // 这里统一使用 |= 直接位或,放在低位就好    if(pc[1]>='a'){        temp|=pc[1]-'a'+10;    }else if(pc[1] >= 'A'){        temp|=pc[1]-'A'+10;            }else{        temp|=pc[1] - '0';    }    // 这里再做一次位与,一眼看去没怎么搞懂这个的作用    // 这里就十分有趣了,后面讲嘻嘻    return(temp & 0x0FF);    // 这里的一句话就是很强了,直接使用条件表达式        //return (((pc[0] >= 'A') ? ( pc[0] - 'A' + 10 ) : ( pc[0] - '0' ) ) << 4 |     //        ((pc[1] >= 'A') ? ( pc[1] - 'A' + 10 ) : ( pc[1] - '0' ) )) & 0x0FF;}

这里比较好玩的一点,就是这个 &0xff 看上去的确是没啥作用呀。实际上,这里就有了符号位这样的一个东西.

记住,我们的输入的数据只是有两位十进制,对吧,所以分别在b0~b3,和b4~b7,所以说,我们使用了这个char的8位数据,不过事实上,这里的问题是什么???

在PC上面呀,char是16位的。最高位的数据我们是没有用到的的。这里存在的符号位,当然会影响我们的值得真实大小,所以使用 0&ff 实际上应该写成 & 0x00ff。这样前面用不到的地方全部清零,就没有了符号位的影响

  • byte为什么要与上0xff

  • // 打印缓冲区内容,没啥好讲的
    void dumpHex(uint8* buf, uint32 len)
    {

    uint32 i=0;for(i=0; i<len; i++){    printf("%02X ", buf[i]);}putchar('\n');

    }

    // CRC 的实现过程
    uint8 makeCrc(uint8* buf, uint32 len)
    {

    uint8 crc = 0, i = 0;for(i=0; i<len; i++){    crc -= *buf++;}return crc;

    }
    CRC 的在这里的实现过程,简单的讲一句话,把每字节的值逐字节进行运算。最后得到一个字节的值,这样只能使得一定可能的查错。要是两个值刚好一个加一,一个减一,没办法了


int openPort(const char* dev, unsigned long flags){    return open(dev, O_RDWR | O_NOCTTY | O_NDELAY | flags);}

主函数入口

int main (int argc, const char** argv)

都是逻辑代码,所以这里只是贴出部分的有趣的代码

// 256k 的优雅的分配bin_buff = (uint8*)malloc(256 << 10); //256kBif( !bin_buff ) {    fprintf(stderr, "Could not allocate 256kB buffer\n");        goto Error;}memset(bin_buff, 0xFFFFFFFF, (256 << 10));

设备握手

这里的握手过程,典型的业务代码吧。发送握手,接收,之后判断 XD

#define BOOTLOADER_HELLO_STR "\xC1"//send HELLOres = write(dev_fd, BOOTLOADER_HELLO_STR, 1);res = readWithTimeout(dev_fd, buffer, 4, 3);if( res != 4 || buffer[3] != BOOTLOADER_OK ) {    puts("ERROR");    fprintf(stderr, "No reply from the bootloader, or invalid reply received: %d\n", res);    fprintf(stderr, "Please make sure that PGND and PGC are connected, replug the device and try again\n");    goto Error;}puts("OK\n"); //extra LF for spacingprintf("Device ID: %s [%02x]\n", (buffer[0] == 0xD4) ? "PIC24FJ64GA002" : "UNKNOWN", buffer[0]);printf("Bootloader version: %d,%02d\n", buffer[1], buffer[2]);if( buffer[0] != 0xD4 ) {    fprintf(stderr, "Unsupported device (%02x:UNKNOWN), only 0xD4 PIC24FJ64GA002 is supported\n", buffer[0]);    goto Error;}

错误处理

很多地方到处宣扬着 goto 有害论.实际上,在C这个异常处理尚不健全的情况下。使用Goto 实现异常处理的方法,是十分OK的。

源程序的后面,实现了两个异常处理的标号:

Finished:    if( bin_buff ) {         free( bin_buff );    }    close(dev_fd);    return 0;Error:    if( bin_buff ) {        free( bin_buff );    }    if( dev_fd >= 0 ) {        close(dev_fd);    }    return -1;

熟读代码三千行,不会编程也会背。2333

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注