This page was mostly written for the initial DryOS porting effort circa 2008, and is now largely obsolete. DryOS cameras are now well supported in CHDK and the normal CHDK development process. The general reverse engineering information found here is still useful, but readers looking for porting information should refer to Adding support for a new camera and For Developers. |
OS Switch[]
Since late 2007 Digic-based cameras are shipped using Canon's own Operating System named DryOS. It replaces VxWorks from Wind River which has been used before on Digic II and some Digic III cameras. DryOS had existed before and was in use in other Canon hardware, such as digital video cameras and high-end webcams. The reason for the switch were most probably royalties Canon had to pay for VxWorks.
Cameras[]
As of now, these cameras use DryOS.
- A470
- A480
- A580
- A590IS
- A650IS
- A720IS
- A1000
- A1100
- A2000
- A2100
- D10
- E1
- G5 X
- G7 X
- G9
- G9 X
- G10
- G11
- IXUS80IS / SD1100IS
- SD1200IS/ IXUS95IS
- IXUS85IS / SD770IS
- IXUS100IS / SD780IS
- IXUS90IS / SD790IS
- SD890IS / IXUS970IS
- SD940IS / IXUS120IS
- SD960IS / IXUS110IS
- SD970IS / IXUS990IS
- SD980IS / IXUS200IS
- SD990IS / IXUS980IS
- IXUS860IS / SD870IS
- IXUS870IS / SD880IS
- IXUS960IS / SD950IS
- S5IS
- S90
- SX1
- SX10
- SX20IS
- SX100IS
- SX110IS
- SX120IS
- SX200IS
- SD210IS
- SD4000
An indicator for DryOS is that the "Firm Update" menu entry appears if a file named *.fi2 is put on the Card instead *.fir (as this was the extension of VxWorks firmwares). DSLRs differ, they use *.fir regardless of OS. In the firmware dump there is a copyright string like
Copyright (C) 1997-2007 by CANON Inc. DRYOS version 2.3, release #0031
and also the string gaonisoy somewhere at the beginning.
Reverse Engineering the Firmware[]
A decent disassembler is necessary to figure out what the firmware does. It is assumed that IDA Pro will be used. There is already an article on loading a powershot-firmware into IDA (Loading dump to IDA). The steps to examine the unknown firmware are basically the same.
- Load firmware into IDA using the correct memory offset.
- Disassemble code.
- Locate functions and name them properly.
- Figure out
- how to call firmware-functions from a C program
- what the camera does when it starts and what it initializes
- how to hook own tasks into the system
- Write a test program to check the discoveries using the bootdisk-method.
- Figure out how to enable the "Firm Update"-Menu entry (which does exist) to get an alternative to the bootdisk-method.
Load into IDA and Disassemble code[]
First I recommend reading Loading dump to IDA but don't follow the description just yet since some things are different.
To start with a new firmware you do:
- select "new disassembly" selecting "binary/raw File"
- choose your dump, keep "analysis options" unchecked (defaults are ok).
- select processor "ARM"
- keep "start anlysis now" checked.
- create a ROM section (PowerShot A-series, S/SD/SX/G-Series start address is 0xFF810000) [intrinsic: is this a typo? On the S5 1.01b firmware it's 0xFF800000, my IDA doesn't like 0xFF810000] [Answer: The dump should be loaded to the address it was dumped from. For the reason why the IDA refuses to load whole dump read a little bit below.]
- Rom Start address: 0xFFC00000
- Rom size: 0x003FFFFF
This is a little odd since the dump is actually one byte larger. But since 0xFFFFFFFF - which would be the last byte of the firmware - is a reserved address in IDA we can't use it and have to chop off the last byte from the dump. - Loading address: 0xFFC00000
- Loading size: 0x003FFFFF
- Press <OK> and discover the next odd thing. Although instructed to create a ROM Segment from 0xFFC00000 to 0xFFFFFFFE the segment's size is only one byte. Since the Dump is loaded completely we simply have to grow the ROM section.
To do this select edit->Segments->Edit Segment and set the "End address" from 0xFFC00001 to 0xFFFFFFFE
'Click on 'Open signatures window' tool-button. Or press 'Shift+F5'. '[1]
- Now Apply the CHDK.idc. It will not do everything it did on a VxWorks-Firmware but it disassembles most of the code. [Update: There is CHDK.idc adapted for DryOS.] Note that after running CHDK.idc the entry-code which started at the very beginning of the Firmware will not be disassembled. To get this chunk, you need to unassign (key 'u') the string which is at offset +4 ("gaonisoyP") since the "P" is part of the next instruction and IDA won't disassemble assigned memory locations. After that you can set the cursor to the first byte of the firmware and press 'c' to create the code at this location.
- Fix missing code. IDA stops disassembling when it thinks a function has ended. Unfortunately it thinks wrong sometimes. One particular reason to end disassembling is a BL-command. BL is "Branch with Link". It jumps to a subroutine and returns execution afterwards. In theory it is possible that a called subroutine does not return, but in the PowerShot-Firmware this is 99% not the case. So we need to fix that behaviour. Save the following code as 'code_fix.idc' and run it in IDA. It will look for BLs preceding unknown sections and disassemble it.
#include <idc.idc> static findCode_fix(sb, se) { auto a, c, w, d, b; c = 0; for (a=sb; a<se; a=a+4) { b = Byte(a-1); // byte with jump-opcode (0xEB) d = Byte(a-2); // distance of the jump (relative to current pos), most significant byte // fix stopped MakeCode after "BL xxx" if ((b==0xEB) && isUnknown(GetFlags(a))) { // previous command: BL ... (short jump) if ( (d > 0xf8) || (d < 0x06) ){ // make sure, distance is some reasonable value (i.e. not to large) Message("Jump (%x) found at %x\n", d, a); MakeCode(a); c = c+1; } } } Message( "Code found %d times\n", c); } static main() { auto sb, se, b; Message("*** START OF ANALYSIS ***\n"); sb = MinEA(); se = MaxEA(); Message("Searching for code...\n"); MakeCode(sb); findCode_fix(sb, se); Message("Please wait...\n"); Wait(); Message("*** END OF ANALYSIS ***\n"); }
What you get by now is far from perfect. A lot of things need adjustment. Typical things that need to be fixed are
- Subroutines that are declared shorter than they are.
You will get this one very often. A subroutine is recognized, gets a large comment saying === S U B R O U T I N E === and a few lines of code later there's another comment saying End of function ... although it is clearly not the end.
To fix this skip forward in the code until the actual end of the subroutine. Remember the address right after the end of the function, put the cursor back into the function code, press <Alt>-p to edit the function and set the End address to the noted address.
Alternately, place the cursor on the new last line of the function and press the (lower case) 'e' key. You will be asked if you really want to change the end address: type 'y' or press RET (ENTER on some keyboards). Correct lengths of subroutines are necessary to get proper xref-trees. - Code isn't disassembled at all.
This happens when IDA stopped processing a function or didn't find a reference to a function at all. Just put the cursor on the first unknown byte and press either 'c' to create code that is meant to belong to another subroutine or press 'p' to create a new subroutine. If you're unsure try 'c' first. If the disassembled code looks like a subroutine, you can still press 'p'. - incorrect ASCII strings.
It sometimes happens that the start of a string isn't recognized. This can be confusing since references to this string aren't resolved correctly. In that case put the cursor on the first character of the string and press 'a'. The complete string will be labeled and xref'd correctly.
Firmware Begin[]
String "gaonisoy" in Beginning of Firmware.
ROM:FFC00000 B EntryPoint ROM:FFC00000 ; --------------------------------------------------------------------------- ROM:FFC00004 aGaonisoy DCB "gaonisoy" ROM:FFC0000C ; --------------------------------------------------------------------------- ROM:FFC0000C EntryPoint ; CODE XREF: RestartDevice:loc_FFC00000 ROM:FFC0000C LDR R1, =0xC0410000 ROM:FFC00010 MOV R0, #0
Locate and name functions[]
Since most of the code is now disassembled we can now start to give it proper names or - if that's not possible - at least something more meaningful than sub_FFDF448C.
GrAnd has partial signatures and some idc's for processing DryOS dumps in IDA they can be found here or here.
Assert[]
The easiest thing to start with is the assert-function. Throughout the code there are things that may never fail. If the do something has gone so terribly wrong that a generic error-handler is called and the running function is aborted. That generic error-handler is Assert.
For debugging-purposes Assert writes the name of the source-file as well as the line-number somewhere. So whenever we see something like
CMP R1, #0 ADREQ =aSourceFile ; "SourceFile.c" MOVEQ R1, #0x7f BLEQ Assert
We know (at least) that this code comes from a file named "SourceFile.c" and that the condition that jumps to the Assert-function is something fatal and we don't need to investigate that. We can also name that function to something somewhat meaningful. I used the prefix 'u' for 'unknown' or 'unsure' followed by the name of the sourcefile an (optionally) a number. So this function would be called "uSourceFile1". Whenever it is called from somewhere else, IDA will show its name and we know we've already seen it and will be given a clue what it might do.
To find a function, in that case "Assert" we use the Strings window. It should have been populated by IDA with the strings in the Dump. Click the Strings Windows and press <Alt>-t to search for a string. Enter "assert" and click "OK". The cursor jumps to the next occurrence. Press <Ctrl>-t to search for the next match. The exact string we are looking for is \nAssert: File %s Line %d\n.
If you found it, doubleclick the line in the Strings Window and the Disassembler view will scroll to the position in the firmware. If you found the right location you will see (addresses may differ):
ROM:FFC0C0C8 aAssertFileSLin DCB 0xA ; DATA XREF: sub_FFC0C098+14 ROM:FFC0C0C8 DCB "Assert: File %s Line %d",0xA,0
The IDA-View now shows the position in the Firmware that contains that string. The comment tells us, where this string is referenced (DATA XREF: sub_FFC0C098+14). Doubleclick on "sub_..." to jump to the actual code. You will see (again, addresses may differ)
ROM:FFC0C098 sub_FFC0C098 ; CODE XREF: sub_FFC05088+34j ROM:FFC0C098 ; sub_FFC057B0+30p ... ROM:FFC0C098 LDR R2, =0x1B08 ROM:FFC0C09C LDR R2, [R2] ROM:FFC0C0A0 CMP R2, #0 ROM:FFC0C0A4 MOVEQ R2, R1 ROM:FFC0C0A8 MOVEQ R1, R0 ROM:FFC0C0AC ADREQ R0, aAssertFileSLin ; "\nAssert: File %s Line %d\n" ROM:FFC0C0B0 BEQ sub_FFC00EE8 ROM:FFC0C0B4 BXNE R2 ROM:FFC0C0B4 ; End of function sub_FFC0C098
We can see that this function prepares the String "Assert: File <filename_here> Line <linenumber>" and jumps somewhere else. I tried to follow what it does, but it was to confusing and I was just satisfied with the amount of information the repeated assertations give me.
Since we found this function, we name it properly. Make sure the cursor is in the function code and press <Alt>-p. This opens the "Edit function" dialog. In the field "Name of function" replace "sub_FFC0C098" with "Assert" and click ok.
Every reference to this function will be named "Assert" and is therefore easily understandable. Note that in the "DryOS Canon Firmware; A720-based" signatures, this function is named DebugAssert.
RegisterEventProcedure[]
Many functions are named and registered by a function called RegisterEventProcedure and a few other, partly derived functions.
It usually works like this:
- LDR R0, =FunctionAddress
- ADR R1, aFunctionName
- BL RegisterEventProcedure
By knowing the address of RegisterEventProcedure it's pretty easy to spot registration-functions that do nothing but registering a bunch of other functions.
First we look for RegisterEventProcedure. As with Assert we salvage debug-information to discover and name functions. In this case we are looking for the string "RegisterEventProcedure: INVALID_HANDLE". This is an error message the hunted function may return and a hint for us where to look.
Again, use the strings Window to search for "RegisterEventProcedure: INVALID_HANDLE". If you found it, navigate to the IDA-View with the string and after that to the function referring the string. You should see:
ROM:FFC0C13C sub_FFC0C13C ; CODE XREF: sub_FFC0C1D0+4�j ROM:FFC0C13C ; sub_FFC0C1D8+4�j ... ROM:FFC0C13C ROM:FFC0C13C var_18 = -0x18 ROM:FFC0C13C ROM:FFC0C13C ; FUNCTION CHUNK AT ROM:FFC0C1B0 SIZE 0000001C BYTES ROM:FFC0C13C ROM:FFC0C13C STMFD SP!, {R3-R7,LR} ROM:FFC0C140 MOV R7, R0 ROM:FFC0C144 LDR R4, =0x1B0C ROM:FFC0C148 MOV R0, #0 ROM:FFC0C14C MOV R6, R2 ROM:FFC0C150 MOV R5, R1 ROM:FFC0C154 STR R0, [SP,#0x18+var_18] ROM:FFC0C158 LDR R0, [R4,#8] ROM:FFC0C15C MOV R1, R7 ROM:FFC0C160 MOV R2, SP ROM:FFC0C164 BL sub_FFC1A2E4 ROM:FFC0C168 CMP R0, #7 ROM:FFC0C16C ADREQ R1, aRegistereventp ; "RegisterEventProcedure: INVALID_HANDLE" ROM:FFC0C170 BEQ loc_FFC0C198 ROM:FFC0C174 CMP R0, #0 ROM:FFC0C178 LDREQ R0, [SP,#0x18+var_18] ROM:FFC0C17C BLEQ sub_FFC1979C ROM:FFC0C180 MOV R0, #8 ROM:FFC0C184 BL sub_FFC19798 ROM:FFC0C188 CMP R0, #0 ROM:FFC0C18C STR R0, [SP,#0x18+var_18] ROM:FFC0C190 BNE loc_FFC0C1B0 ROM:FFC0C194 ADR R1, aRegistereven_0 ; "RegisterEventProcedure: ALLOC_ERROR" [...]
Now use the edit function dialog to change the name to RegisterEventProcedure. Right below RegisterEventProcedure there are two other functions which do nothing but setting R2 to 0 and branching to Reg.... Name them RegisterEventProcedure_im1 and ..._im2 since they will be called as well.
Even a bit further down there is the reverse. It writes the string "UnRegisterEvntProc: " and that's just its name. So call this one UnRegisterEvntProc.
The general Rule for error messages is "name_of_function: what went wrong". This rule is applied by the Canon developers and we can make heavy use of it.
One more function calls RegisterEventProcedure. It is called ExportToEventProcedure and used to register the libc-functions. It is registered along with a lot of other functions. Use the String Window to find ExportToEventProcedure. It is right after "ExecuteEventProcedure" and libc-function-names like "strcpy" and "sprintf".
When you found it, jump to the referencing code and you will see
ROM:FFDD80B4 LDR R1, =sub_FFC0CC20 ROM:FFDD80B8 ADR R0, aExporttoeven_0 ; "ExportToEventProcedure" ROM:FFDD80BC BL sub_FFC0CC20
This is somewhat funny. ExportToEventProcedure is called to register itself. The address assigned to R1 is the function address. The address assigned to R0 is the name of the function. Click on =sub_xxxxxxxx and press <Alt>-<Enter>. This opens another IDA-View with the referenced code. Change the function name to ExportToEventProcedure and close the new Window. All BLs to ExportEventProcedure should now use the correct name instead of the address.
Continue renaming all other functions that are registered here.
The same game goes for the "PT_"-Functions. Search for "PT_GetSystemTime" to get you started.
Also look for "SSAPI::" and "drysh". At the front of the "SSAPI::" functions, you'll find the name loaded into R0 and then a call to a set of common functions, the first being a "Log" function, a check for a HardwareError, and a check for LowBattery status. If you search for all calls to the "Log" function, you will be led to an enormous number of functions that you can now name.
When you did all these the Firmware should be a little easier to understand.
enable/disable IRQs[]
Here's the function code that enables/disables Interrupts. I accidentially ran into these. They are named nowhere and are hard to find without knowing what to look for.
ROM:FFC00578 ; =============== S U B R O U T I N E ======================================= ROM:FFC00578 ROM:FFC00578 ; Returns: R0: previous state of IRQ-Flag ROM:FFC00578 ; 0x00 => irq enabled ROM:FFC00578 ; 0x80 => irq disabled ROM:FFC00578 ROM:FFC00578 IRQdisable ; CODE XREF: sub_FFC00A0C+70�p ROM:FFC00578 ; sub_FFC00F24+8�p ... ROM:FFC00578 MRS R1, CPSR ROM:FFC0057C AND R0, R1, #0x80 ROM:FFC00580 ORR R1, R1, #0x80 ROM:FFC00584 MSR CPSR_cxsf, R1 ROM:FFC00588 RET ROM:FFC00588 ; End of function IRQdisable ROM:FFC00588 ROM:FFC0058C ROM:FFC0058C ; =============== S U B R O U T I N E ======================================= ROM:FFC0058C ROM:FFC0058C ; Input: R0: desired state of the IRQ-bit ROM:FFC0058C ; 0x00 => IRQ enabled ROM:FFC0058C ; 0x80 => IRQ disabled ROM:FFC0058C ROM:FFC0058C IRQrestore ; CODE XREF: sub_FFC00A0C+8C�p ROM:FFC0058C ; sub_FFC00F24+28�p ... ROM:FFC0058C MRS R1, CPSR ROM:FFC00590 BIC R1, R1, #0x80 ROM:FFC00594 ORR R1, R1, R0 ROM:FFC00598 MSR CPSR_cxsf, R1 ROM:FFC0059C RET ROM:FFC0059C ; End of function IRQrestore ROM:FFC0059C ROM:FFC005A0 ROM:FFC005A0 ; =============== S U B R O U T I N E ======================================= ROM:FFC005A0 ROM:FFC005A0 ROM:FFC005A0 IRQenable ; CODE XREF: sub_FFC049B0+8�p ROM:FFC005A0 ; sub_FFC049C8+8�p ... ROM:FFC005A0 MRS R1, CPSR ROM:FFC005A4 BIC R1, R1, #0x80 ROM:FFC005A8 MSR CPSR_cxsf, R1 ROM:FFC005AC RET ROM:FFC005AC ; End of function IRQenable
Short ARM ASM Overview[]
This is a short overview over regular ARM commands as well as some compiler-generated nastiness. Full detail is available in the ARM-Reference-guides linked in the reference-section.
Common commands include
- LDR Load memory into a register
- STR write register-contents to memory
- CMP/CMN/TST evaluate Expression and set Status-Flags accordingly
- B Branch. Continues program execution at the given location
- BL Branch with Link. Continues program execution at given location and stores current location in LR (Link Register).
- BX Branch and change execution mode (between ARM and Thumb mode)
Nearly every command can evaluate the status flags. For that a condition-suffix is added to the command. The command only executes if the condition is met. E.g. B means "branch", BCS means "branch if Carry Set" The condition-suffixes are (copy-n-paste from the Technical Reference Manual):
- EQ Equal
- NE Not equal
- CS Unsigned higher, or same
- CC Unsigned lower
- MI Negative
- PL Positive, or zero
- VS Overflow
- VC No overflow
- HI Unsigned higher
- LS Unsigned lower, or same
- GE Greater, or equal
- LT Less than
- GT Greater than
- LE Less than, or equal
- AL Always
Registers and other CPU-Specific things
- R0 - R12 are general purpose 32bit wide registers. In CHDK, R12 is sometimes referred to as IP.
- R13, a.k.a. SP is the Stack Pointer. Although I haven't seen it being referred to as R13 in IDA.
- LR is the Link Register. It is set by the BL command
- PC is the Program Counter. It holds the address of the currently executed command (this is not 100% accurate, see the manual for details).
- CPSR is the Current Program Status Register. It holds the Status Flags and several internal Information.
- SPSR is the Saved Program Status Register. I haven't seen this in the Firmware.
Specific code-constructs can be seen more than once. Here are some:
- Entering and leaving subroutines.
On executing BL the CPU continues execution at the location in the argument and stores the old location in the LR (Link Register). At the beginning of the subroutine you will often see a command like STMFD SP!,{R4-R7,LR}. This stores Registers R4, R5, R6, R7 as well as LR to the Stack and increases the Stack Pointer (SP). The particular Registers saved will differ depending on which will be used by the subroutine.
At the end of the subroutine you will usually see something like LDMFD SP!,{R4-R7,PC}. This will take Values from the Stack and store them in R4-R7 as well as PC. Note that the value that was read from LR is stored to PC (the Program Counter) causing the execution to be continued in the calling function.
Also note, that Registers other than R4-R7 will remain in the state in which the subroutine left them. Usually R0 and R1 are used to share values between caller and callee. - A modification is STMFD SP!,{R4-R7,LR} at the start and LDMFD SP!,{R4-R7,LR}; B <sub_2> at the end of a function. This subroutine restores the initial state including the LR and branches to another subroutine. When <sub_2> returns, it will directly return to the original caller.
Referencing memory. Some usual memory-referencing methods
- LDR R0, R1 loads the value of R1 to R0
- LDR R0, [R1] loads the value of the address referenced by R1 to R0
- LDR R0, [R1, #1] same as above, referenced addr is R1 + 1
- LDR R0, [R1], #1 same as above, increases R1 by 1 afterwards
To store an immediate value in a Register you use MOV
- MOV R0, #0 writes 0 to R0
Meaning of multiple Registers as Arguments
- ADD R0, R1, R2 general rule: R0 is the target, R1 and R2 are operands. In that case: Add R1 and R2, store the result in R0.
- ADD R0, R0, R1 Adds R1 to R0