Homepage Johann Hanne 
Valid XHTML 1.0 Strict
About | Projects | Gadgets | Photos | Links
Projects - Actiontec DPCM stuff - The binary module 

The original Actiontec Dual PC Modem firmware is shipped with a binary Linux kernel module called cnxtserial_buildin. This module drives the internal V.92 modem which can be talked to via /dev/ttyS2. At the very beginning of the actionhack project it was clear that it's not some kind of annoying "winmodem" driver where the signal processing is done by the main CPU. Instead the module only provides the interface to the modem chip. It was unclear however what the module is doing exactly. Maybe it sets up the modem by poking around in its registers? Maybe it loads the modem firmware?

If I would have been able to load that binary module with later kernels (the original kernel is as old as 2.4.6) I would never have wasted my time by looking at it. However, the module didn't load with 2.4.29uc0 because of unresolved symbols, so I took a closer look at it - maybe I could hack it up so that it loads. But while looking at it I noticed that many function names correspond to the ones in cnxtserial.c (which is part of the uClinux kernel). After looking even closer it got clear to me that most of the function names match. My first conclusion was that the interface to the internal modem is the same as the interface defined in cnxtserial.c and that the cnxtserial_buildin module only initializes the modem in some way, maybe by telling it to "talk serial", as it had been discussed on the mailing list before. So I decided to copy cnxtserial.c to cnxtserial_buildin.c, compile it and compare its disassembly to the disassembly of the original cnxtserial_buildin.o file.

There were quite some differences in the assembler code. The first things I noticed were two functions named "get_modem_info" and "set_modem_info" which were in the original module but not in my rebuilt module. "set_modem_info" sounded like some modem initialization stuff, so I focused on it. My idea was to find out where the set_modem_info function gets called, then copy the (compiled) function from the original module and then call it in the rebuilt module, too. With some luck, this initialization is the only thing we need to get the modem work. Unfortunately there was a big problem: the branch instructions in a Linux kernel module don't "point" to the correct position. This is because at compile time it's unknown to what memory address a module gets loaded. So the branch target addresses have to be filled in ("relocated") at module load time. This work is usually done by the userspace insmod tool (busybox in embedded devices). I asked on the uClinux mailing list if there is an easy way to get the relocations done within the file without loading it, but I didn't get a positive response. So I had to do some more work to actually know where the set_modem_function is called. I hacked up the busybox insmod applet so that it prints out information about every relocation that it does. I then wrote a C program for my x86 workstation that takes the busybox/insmod output and does the relocations in the cnxtserial_buildin.o file. After that the disassembly of the kernel module indeed showed the branch instructions pointing to the correct branch targets (I did know where the branch instructions must point to because they had to be similar to the function calls in the cnxtserial.c source). And oh well, I got disappointed again: The only branches to set_modem_info (and get_modem_info) were within rs_ioctl, and rs_ioctl itself was not called anywhere in the module. Additionally, there couldn't be any userspace program that makes a ioctl call which branches to set_modem_info, simply because the modem works by loading the module without calling any of Actiontec's userland programs. So finally it turned out that I had to go the hardest way of all: analyze every single disassembled function which differs between cnxtserial and cnxtserial_buildin and then try to understand the assembler instructions so that I can do the same thing in the C source. I had a hard time doing this because I never worked with assembler (especially not ARM assembler) before. But after looking at it again and again I at last understood the most basic instructions (ldr, str, cmp, tst, b*, etc.) and was able to rebuild the C source code. Finally it turns out that the main difference between cnxtserial and cnxtserial_buildin is some kind of handshake between CPU and modem chip. The most obvious example for this is the wait_EOT function.

cnxtserial.c wait_EOT():

static void wait_EOT(struct uart_regs *uart)
{
  volatile struct uart_regs* puart;
  unsigned long timeout = 10000;
  puart = (volatile struct uart_regs*)uart;
  while(1){
    if(timeout-- == 0) {
      printk("eot timeout\n");
      break;
    }
    if(puart->line_status_reg & LSR_Tx_Fifo_Empty)
      break;
  }
}

cnxtserial_buildin.c wait_EOT():

static void wait_EOT(struct uart_regs *uart)
{
  volatile struct uart_regs* puart;
  unsigned long timeout = 10000;
  unsigned short modem_ctrl;
  puart = (volatile struct uart_regs*)uart;
  while(1){
    if(timeout-- == 0) {
      printk("eot timeout\n");
      line_status(uart);
      break;
    }
    if(puart->line_status_reg & LSR_Tx_Fifo_Empty) {
      break;
    } else {
      modem_ctrl=puart->modem_ctrl;
      if(!(puart->msr & 0x80)) {
        return;
      }
    }
  }
}

If you want more details about the differences, just make a context diff between cnxtserial.c in the uClinux source tree and cnxtserial_buildin.c in the downloadable archive below.

One last thing to note is that Actiontec and/or Conexant violates the GPL here again. cnxtserial.c is heavily based on serial.c, e.g. the serial_paranoia_check function has been copied literally. Moreover, other functions are identical/similar (e.g. do_serial_bh, do_softint) and the overall driver structure is the same. Thus, cnxtserial.c is not exclusively property of Actiontec/Conexant, it also contains GPL code, so it also must be put under the GPL (if it would contain Actiontec/Conexant code only, they could put it under GPL the and under a proprietary license at the same time). Consequently, the cnxtserial_buildin module contains GPL code and not giving out its source code violates the GPL.

Enough written, here is the cnxtserial_buildin archive, look at the included CONTENTS file for a description of the single files: cnxtserial_buildin.tar.gz

As I wrote already I compared the assembler code of all functions in the original module and the rebuilt module. For several reasons, they do not match 100%:

  • The gcc (vanilla 2.95.3) I used seems to prefer the STMDB instruction over STR and LDMIA over LDR. Maybe Actiontec used another gcc version or slightly different optimization setting. Affected functions:
    GPIOB5_Handler, cleanup_module, cnxt_console_setup, do_serial_bh, do_serial_hangup, tx_start
  • The functions contain pointers, e.g. to strings which are output by printk. The pointers sometimes differ because the modules are not 100% equal in size and thus the contents where the pointers point to are at different locations. Affected functions:
    cleanup_module, cnxt_put_char, rs_set_ldisc, send_break, serial_cnxt_console_init, wait_EOT, init_module, rs_open, rs_interrupt_buildin
  • The gcc I used sometimes uses a register for addressing a memory position at a certain offset instead of addressing the offset directly. E.g. the original module uses:
    ldrb   r2, [r5, #272]
    
    Whereas the rebuilt module uses:
    mov    r3, #272
    ldrb   r2, [r5, r3]
    
    I don't know why my gcc is doing this. Maybe this leads to faster code if the offset value can be reused later. Affected functions:
    rs_flush_chars, rs_open, rs_set_termios, rs_close, rs_interrupt_buildin, rs_write
  • As a consequence of the last point, some functions have a different code size, leading to different offsets within the function. Affected functions:
    rs_write, rs_close, rs_interrupt_buildin
  • As a further consequence, registers which are used for storing offsets can't obviously be used for storing other stuff. Thus sometimes different registers are used for the same C variable. Affected functions:
    wait_EOT, init_module, rs_close, rs_interrupt_buildin, rs_open

In addition to these very minor differences in the machine code (which make absolutetly no differnce in functionality), two things are missing from the original cnxtserial_buildin module:

  • The ioctl-functions implemented by set_modem_info and get_modem_info. But don't worry, these functions seem to be some kind of debug functions and are probably not used by the original firmware anyway.
  • The receive_chars function (which is inlined into the rs_interrupt_buildin function) in the original cnxtserial_buildin module outputs "no tty: ...some-string..." via printk if something is received from the modem but /dev/ttyS2 has been closed by the userland process. The "...some-string..." is the string which has been received by the modem and was then discarded. The rebuilt module only outputs "no tty", which does not really matter.