so yesterday I digged a bit in the issue related to the OSDK generating invalid code when adding some code in a C program.
What I did, is to add parameters to the program, in a function call. And I immediately experienced code not working as planned. The interesting thing being that the new function was not even being called, just having the code present was enough to cause the issue.
So it appears that for some reason, the code bellow is corrupting data, the only reason I can find is an issue with the stack management (stack being at the end).
Since it may take some time before I find the problem, I decided to explain how the whole thing work.
1) The project
I'm going to take as an example the code from Barnsey, located in the SVN server in "users/barnsey123". My SVN depot is mapped on E:\SVN, so in the E:\svn\users\barnsey123 folder I have:
- main.c
- osdk_build.bat
- osdk_config.bat
- osdk_execute.bat
When I run the osdk_build.bat a BUILD folder appear, with the following content:
- final.out
- symbols
- viking.tap
- xaerr.txt
When I run the osdk_execute.bat my emulator is started, and it loads viking.tap. The question is, what happens in-between that leads to the transformation of main.c in viking.tap?
2) The build process
The important parameters are set in the osdk_config.bat:
Code: Select all
SET OSDKADDR=$800
SET OSDKNAME=VIKING
SET OSDKFILE=MAIN
Make.bat will process each file, and based on the extension found in the source folder, will treat it as a BASIC file (.bas), ASSEMBLER file (.s) or C file (.c).
- The BASIC files are converted using BAS2TAP
- The ASSEMBLER files are directly copied to the TMP folder
- The C files are compiled to assembler and stored in the TMP folder
Then at the end the various files are linked together, and assembled by XA to generate the final binary executable.
In this particular case, we have only one file, MAIN.C. Let's see how it's converted from C to 6502 assembler.
3) The C compiler
If you build the program, and then take a look in %osdk%\TMP you will find the following files:
- MAIN.c
- MAIN.c2
- MAIN.s
- MAIN
plus two files we will talk about later:
- link.bat
- linked.s
The first file, MAIN.c is still a C program, but it contains the pre-processed content of the original main.c. This means that there are no more #include commands (the files have been inlined in the main source file), there are no more comments (they have been removed), and every single #define parameter has been replaced by its real value.
So it's still C, but without any external dependencies.
So in this particular case, it converted the original drawbox function from:
Code: Select all
// DRAWBOX - draw a box at x,y length xbox
void drawbox(int x, int y, int z)
{
/*
x=xcoord for curset
y=ycoord for curset
z=size of line to draw
*/
curset(x,y,1);
draw(z,0,1);
draw(0,z,1);
draw(-z,0,1);
draw(0,-z,1);
}
Code: Select all
# 8 "MAIN.c" 2
void drawbox(int x, int y, int z)
{
curset(x,y,1);
draw(z,0,1);
draw(0,z,1);
draw(-z,0,1);
draw(0,-z,1);
}
The second file, MAIN.c2 contains the results of the compilation. It's not 6502 assembler, it's using an intermediate representation using a weird syntax. Our drawbox function looks like this now:
Code: Select all
_drawbox
ENTER(1,6)
INDIRW_AD((ap),4,tmp0)
ASGNW_DC(tmp0,reg0)
INDIRW_AD((ap),0,tmp0)
ARGW_D(tmp0,(sp),0)
INDIRW_AD((ap),2,tmp0)
ARGW_D(tmp0,(sp),2)
ARGW_C(1,(sp),4)
CALLV_C(_curset,6)
INDIRW_CD(reg0,tmp0)
ARGW_D(tmp0,(sp),0)
ARGW_C(0,(sp),2)
ARGW_C(1,(sp),4)
CALLV_C(_draw,6)
ARGW_C(0,(sp),0)
INDIRW_CD(reg0,tmp0)
ARGW_D(tmp0,(sp),2)
ARGW_C(1,(sp),4)
CALLV_C(_draw,6)
INDIRW_CD(reg0,tmp0)
NEGI_DD(tmp0,tmp0)
ARGW_D(tmp0,(sp),0)
ARGW_C(0,(sp),2)
ARGW_C(1,(sp),4)
CALLV_C(_draw,6)
ARGW_C(0,(sp),0)
INDIRW_CD(reg0,tmp0)
NEGI_DD(tmp0,tmp0)
ARGW_D(tmp0,(sp),2)
ARGW_C(1,(sp),4)
CALLV_C(_draw,6)
LEAVE
Here is an extract of this file showing the implementation of the ENTER virtual instructions:
Code: Select all
#define ENTER(r,n)\
ldx #n ;\
lda #r ;\
jsr enter ;\
After going through the preprocessor we obtain MAIN.S. Here is our final drawbox assembler code:
Code: Select all
_drawbox
ldx #6 ; lda #1 ; jsr enter ;
ldy #4 ; lda (ap),y ; sta tmp0 ; iny ; lda (ap),y ; sta tmp0+1 ;
lda tmp0 ; sta reg0 ; lda tmp0+1 ; sta reg0+1 ;
ldy #0 ; lda (ap),y ; sta tmp0 ; iny ; lda (ap),y ; sta tmp0+1 ;
lda tmp0 ; ldy #0 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
ldy #2 ; lda (ap),y ; sta tmp0 ; iny ; lda (ap),y ; sta tmp0+1 ;
lda tmp0 ; ldy #2 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
lda #<(1) ; ldy #4 ; sta (sp),y ; iny ; lda #>(1) ; sta (sp),y ;
ldy #6 ; jsr _curset ;
lda reg0 ; sta tmp0 ; lda reg0+1 ; sta tmp0+1 ;
lda tmp0 ; ldy #0 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
lda #<(0) ; ldy #2 ; sta (sp),y ; iny ; lda #>(0) ; sta (sp),y ;
lda #<(1) ; ldy #4 ; sta (sp),y ; iny ; lda #>(1) ; sta (sp),y ;
ldy #6 ; jsr _draw ;
lda #<(0) ; ldy #0 ; sta (sp),y ; iny ; lda #>(0) ; sta (sp),y ;
lda reg0 ; sta tmp0 ; lda reg0+1 ; sta tmp0+1 ;
lda tmp0 ; ldy #2 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
lda #<(1) ; ldy #4 ; sta (sp),y ; iny ; lda #>(1) ; sta (sp),y ;
ldy #6 ; jsr _draw ;
lda reg0 ; sta tmp0 ; lda reg0+1 ; sta tmp0+1 ;
lda #0 ; sec ; sbc tmp0 ; sta tmp0 ; lda #0 ; sbc tmp0+1 ; sta tmp0+1 ;
lda tmp0 ; ldy #0 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
lda #<(0) ; ldy #2 ; sta (sp),y ; iny ; lda #>(0) ; sta (sp),y ;
lda #<(1) ; ldy #4 ; sta (sp),y ; iny ; lda #>(1) ; sta (sp),y ;
ldy #6 ; jsr _draw ;
lda #<(0) ; ldy #0 ; sta (sp),y ; iny ; lda #>(0) ; sta (sp),y ;
lda reg0 ; sta tmp0 ; lda reg0+1 ; sta tmp0+1 ;
lda #0 ; sec ; sbc tmp0 ; sta tmp0 ; lda #0 ; sbc tmp0+1 ; sta tmp0+1 ;
lda tmp0 ; ldy #2 ; sta (sp),y ; iny ; lda tmp0+1 ; sta (sp),y ;
lda #<(1) ; ldy #4 ; sta (sp),y ; iny ; lda #>(1) ; sta (sp),y ;
ldy #6 ; jsr _draw ;
jmp leave ;
4) The linker
The final executable source code is generate by the linker, which collects all the converted source codes, find all the external references (function calls like printf or get) referenced in the %osdk%/library.ndx file, and finally adds the header.s and tail.s (in the same folder) to provide support for the C stack and stack frame management.
The result of the linker's work is the %osdk%/tmp/linked.s, which is then sent to XA.EXE for finally creating the final binary.
Now that we've explained everything, let's take a look at the problem.
5) The problem
And easy way to visualise the problem is to modify the program in the following way.
Modify readkeyb from this:
Code: Select all
void readkeyb() // filter keyboard input
{
int key=0;
int cx=98;
int cy=86;
Code: Select all
void readkeyb(int cx,int cy) // filter keyboard input
{
int key=0;
Code: Select all
readkeyb();
Code: Select all
readkeyb(98,86);
Just by doing that the program stop drawing things correctly. The board only shows one single tile on the top left.
The interesting thing is that the readkeyb function is called AFTER the drawboard, so the modified code is never actually called.
The second thing, is that if you comment out the call to readkeyb, then the program runs fine.
The culprit at this point seems to be the compiler which probably generates wrong code (after all the whole process is very convoluted, no?) but if you compare both linked.s files, the only difference in the generate assembly code is the missing lines that correspond to the call of readkeyb that we just commented out:
Code: Select all
lda #<(98)
ldy #0
sta (sp),y
iny
lda #>(98)
sta (sp),y
lda #<(86)
ldy #2
sta (sp),y
iny
lda #>(86)
sta (sp),y
ldy #4
jsr _readkeyb
At this point the only idea I have is that for some reason the added code is triggering some issue in the addresses/function calls, possibly because the Enter/Leave code is at the top of the executable while the stack is at the very end.
Stay tuned for more information, and please don't hesitate sharing your thoughts.