mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-27 06:13:13 +00:00
1105 lines
24 KiB
C++
1105 lines
24 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code is free software; you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code is distributed in the hope that it will be
|
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
|
|
#include <hash_map>
|
|
|
|
struct VanillaStringCmp
|
|
{
|
|
enum { bucket_size = 4, min_buckets = 8 }; // parameters for VC hash table
|
|
|
|
size_t operator()( const char* s ) const // X31 hash
|
|
{
|
|
size_t h = 0;
|
|
const unsigned char* p = (const unsigned char*)s;
|
|
while (*p)
|
|
h = (h << 5) - h + (*p++);
|
|
return h;
|
|
}
|
|
|
|
bool operator()( const char* lhs, const char* rhs ) const
|
|
{
|
|
return (strcmp(lhs, rhs) < 0);
|
|
}
|
|
};
|
|
|
|
|
|
#include "../../qcommon/vm_local.h"
|
|
|
|
extern "C" {
|
|
#include "cmdlib.h"
|
|
};
|
|
|
|
|
|
typedef stdext::hash_map< const char*, int, VanillaStringCmp > OpTable;
|
|
static OpTable aOpTable;
|
|
|
|
|
|
static char outputFilename[MAX_OSPATH];
|
|
|
|
typedef enum {
|
|
CODESEG,
|
|
DATASEG, // initialized 32 bit data, will be byte swapped
|
|
LITSEG, // strings
|
|
BSSSEG, // 0 filled
|
|
JTRGSEG, // pseudo-segment that contains only jump table targets
|
|
NUM_SEGMENTS
|
|
} segmentName_t;
|
|
|
|
#define MAX_SEGSIZE 0x400000
|
|
|
|
struct segment_t {
|
|
const char* name;
|
|
byte image[MAX_SEGSIZE];
|
|
int imageUsed;
|
|
int segmentBase; // only valid on second pass
|
|
};
|
|
|
|
static segment_t segment[NUM_SEGMENTS];
|
|
static segment_t* currentSegment;
|
|
|
|
|
|
struct symbol_t {
|
|
const segment_t* segment;
|
|
int value;
|
|
};
|
|
|
|
typedef stdext::hash_map< const char*, symbol_t*, VanillaStringCmp > SymTable;
|
|
static SymTable aSymGlobal;
|
|
static SymTable aSymImported;
|
|
|
|
static symbol_t* lastSymbol; // symbol most recently defined, used by HackToSegment
|
|
|
|
|
|
struct options_t {
|
|
qboolean verbose;
|
|
qboolean writeMapFile;
|
|
};
|
|
|
|
static options_t options;
|
|
|
|
|
|
#define MAX_ASM_FILES 256
|
|
int numAsmFiles;
|
|
char *asmFiles[MAX_ASM_FILES];
|
|
char *asmFileNames[MAX_ASM_FILES];
|
|
|
|
static int currentFileIndex;
|
|
static const char* currentFileName;
|
|
static int currentFileLine;
|
|
|
|
static SymTable aSymLocal[MAX_ASM_FILES];
|
|
|
|
|
|
// we need to convert arg and ret instructions to
|
|
// stores to the local stack frame, so we need to track the
|
|
// characteristics of the current function's stack frame
|
|
int currentLocals; // bytes of locals needed by this function
|
|
int currentArgs; // bytes of largest argument list called from this function
|
|
int currentArgOffset; // byte offset in currentArgs to store next arg, reset each call
|
|
|
|
int passNumber;
|
|
int instructionCount;
|
|
|
|
|
|
void QDECL Com_Error( int level, const char* error, ... )
|
|
{
|
|
va_list va;
|
|
va_start( va, error );
|
|
vprintf( error, va );
|
|
va_end( va );
|
|
exit( level );
|
|
}
|
|
|
|
void QDECL Com_Printf( const char* fmt, ... ) {}
|
|
|
|
|
|
#ifdef _MSC_VER
|
|
#define INT64 __int64
|
|
#define atoi64 _atoi64
|
|
#else
|
|
#define INT64 long long int
|
|
#define atoi64 atoll
|
|
#endif
|
|
|
|
/*
|
|
BYTE values are specified as signed decimal strings
|
|
A properly functional atoi() will cap large signed values at 0x7FFFFFFF
|
|
Negative word values are often specified by LCC as very large decimal values
|
|
so values that should be between 0x7FFFFFFF and 0xFFFFFFFF come out as 0x7FFFFFFF
|
|
|
|
This function is a trivial (tho clever) hack to work around this problem
|
|
*/
|
|
static int atoiNoCap( const char* s )
|
|
{
|
|
INT64 n = atoi64(s);
|
|
return (n < 0) ? (int)n : (unsigned)n;
|
|
}
|
|
|
|
|
|
static void report( const char* fmt, ... )
|
|
{
|
|
if (!options.verbose)
|
|
return;
|
|
|
|
va_list va;
|
|
va_start( va, fmt );
|
|
vprintf( fmt, va );
|
|
va_end(va);
|
|
}
|
|
|
|
|
|
static int errorCount;
|
|
|
|
static void CodeError( const char* fmt, ... )
|
|
{
|
|
errorCount++;
|
|
printf( "%s:%i : ", currentFileName, currentFileLine );
|
|
|
|
va_list va;
|
|
va_start( va, fmt );
|
|
vprintf( fmt, va );
|
|
va_end( va );
|
|
}
|
|
|
|
|
|
static void EmitByte( segment_t* seg, int v )
|
|
{
|
|
if ( seg->imageUsed >= MAX_SEGSIZE ) {
|
|
Com_Error( ERR_FATAL, "MAX_SEGSIZE" );
|
|
}
|
|
seg->image[ seg->imageUsed ] = v;
|
|
seg->imageUsed++;
|
|
}
|
|
|
|
|
|
static void EmitInt( segment_t* seg, int v )
|
|
{
|
|
if ( seg->imageUsed >= MAX_SEGSIZE - 4 ) {
|
|
Com_Error( ERR_FATAL, "MAX_SEGSIZE" );
|
|
}
|
|
seg->image[ seg->imageUsed ] = v & 255;
|
|
seg->image[ seg->imageUsed + 1 ] = ( v >> 8 ) & 255;
|
|
seg->image[ seg->imageUsed + 2 ] = ( v >> 16 ) & 255;
|
|
seg->image[ seg->imageUsed + 3 ] = ( v >> 24 ) & 255;
|
|
seg->imageUsed += 4;
|
|
}
|
|
|
|
|
|
static void ExportSymbol( const char* symbol )
|
|
{
|
|
// symbols can only be defined on pass 0
|
|
if ( passNumber == 1 )
|
|
return;
|
|
|
|
SymTable::const_iterator it = aSymGlobal.find( symbol );
|
|
if (it != aSymGlobal.end())
|
|
return;
|
|
|
|
const char* name = copystring( symbol );
|
|
symbol_t* s = new symbol_t;
|
|
s->value = 0;
|
|
aSymGlobal[ name ] = s;
|
|
}
|
|
|
|
|
|
static void ImportSymbol( const char* symbol )
|
|
{
|
|
// symbols can only be defined on pass 0
|
|
if ( passNumber == 1 )
|
|
return;
|
|
|
|
SymTable::const_iterator it = aSymImported.find( symbol );
|
|
if (it != aSymImported.end())
|
|
return;
|
|
|
|
const char* name = copystring( symbol );
|
|
symbol_t* s = new symbol_t;
|
|
aSymImported[ name ] = s;
|
|
}
|
|
|
|
|
|
static void DefineSymbol( const char* symbol, int value )
|
|
{
|
|
// symbols can only be defined on pass 0
|
|
if ( passNumber == 1 )
|
|
return;
|
|
|
|
const char* name = copystring( symbol );
|
|
symbol_t* s = new symbol_t;
|
|
s->segment = currentSegment;
|
|
s->value = value;
|
|
|
|
SymTable::iterator it;
|
|
|
|
it = aSymGlobal.find( symbol );
|
|
if (it != aSymGlobal.end()) {
|
|
// we've encountered an "export blah" for this "proc blah"
|
|
// so update the global's placeholder with the real data too
|
|
delete (*it).second;
|
|
(*it).second = s;
|
|
}
|
|
|
|
aSymLocal[currentFileIndex][ name ] = s;
|
|
|
|
lastSymbol = s;
|
|
}
|
|
|
|
|
|
static int LookupSymbol( const char* symbol )
|
|
{
|
|
// symbols can only be evaluated on pass 1
|
|
if ( passNumber == 0 )
|
|
return 0;
|
|
|
|
SymTable::const_iterator it;
|
|
|
|
it = aSymLocal[currentFileIndex].find( symbol );
|
|
if (it != aSymLocal[currentFileIndex].end()) {
|
|
const symbol_t* s = (*it).second;
|
|
return (s->segment->segmentBase + s->value);
|
|
}
|
|
|
|
it = aSymImported.find( symbol );
|
|
if (it == aSymImported.end()) {
|
|
// no explicit prototype == incorrect code
|
|
// whether the symbol is "reachable" or not
|
|
CodeError( "Symbol %s undefined\n", symbol );
|
|
return 0;
|
|
}
|
|
|
|
it = aSymGlobal.find( symbol );
|
|
if (it == aSymGlobal.end()) {
|
|
CodeError( "External symbol %s undefined\n", symbol );
|
|
return 0;
|
|
}
|
|
|
|
const symbol_t* s = (*it).second;
|
|
return (s->segment->segmentBase + s->value);
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
// this stuff should probably be replaced with COM_Parse, but...
|
|
|
|
|
|
#define MAX_LINE_LENGTH 1024
|
|
static char lineBuffer[MAX_LINE_LENGTH];
|
|
static int lineParseOffset;
|
|
static char token[MAX_LINE_LENGTH];
|
|
|
|
|
|
/*
|
|
Extracts the next line from the given text block.
|
|
If a full line isn't parsed, returns NULL
|
|
Otherwise returns the updated parse pointer
|
|
*/
|
|
static const char* ExtractLine( const char* data )
|
|
{
|
|
++currentFileLine;
|
|
|
|
lineParseOffset = 0;
|
|
token[0] = 0;
|
|
|
|
if ( data[0] == 0 ) {
|
|
lineBuffer[0] = 0;
|
|
return NULL;
|
|
}
|
|
|
|
int i;
|
|
for ( i = 0 ; i < MAX_LINE_LENGTH ; i++ ) {
|
|
if ( data[i] == 0 || data[i] == '\n' ) {
|
|
break;
|
|
}
|
|
}
|
|
if ( i == MAX_LINE_LENGTH ) {
|
|
CodeError( "MAX_LINE_LENGTH" );
|
|
return data;
|
|
}
|
|
|
|
memcpy( lineBuffer, data, i );
|
|
lineBuffer[i] = 0;
|
|
data += i;
|
|
|
|
if ( data[0] == '\n' ) {
|
|
return ++data;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
// parse a token out of lineBuffer
|
|
|
|
static qboolean GetToken()
|
|
{
|
|
token[0] = 0;
|
|
|
|
// skip whitespace
|
|
while ( lineBuffer[ lineParseOffset ] <= ' ' ) {
|
|
if ( lineBuffer[ lineParseOffset ] == 0 ) {
|
|
return qfalse;
|
|
}
|
|
++lineParseOffset;
|
|
}
|
|
|
|
// skip ; comments
|
|
int c = lineBuffer[ lineParseOffset ];
|
|
if ( c == ';' ) {
|
|
return qfalse;
|
|
}
|
|
|
|
// parse a regular word
|
|
int len = 0;
|
|
do {
|
|
token[len++] = c;
|
|
c = lineBuffer[ ++lineParseOffset ];
|
|
} while (c > ' ');
|
|
token[len] = 0;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static int ParseValue()
|
|
{
|
|
GetToken();
|
|
return atoiNoCap( token );
|
|
}
|
|
|
|
|
|
static int ParseExpression()
|
|
{
|
|
// skip any leading minus
|
|
int i = (token[0] == '-') ? 1 : 0;
|
|
|
|
for ( ; i < MAX_LINE_LENGTH ; i++ ) {
|
|
if ( token[i] == '+' || token[i] == '-' || token[i] == 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
char s[MAX_LINE_LENGTH];
|
|
memcpy( s, token, i );
|
|
s[i] = 0;
|
|
|
|
int v;
|
|
if ( isdigit(s[0]) || (s[0] == '-') ) {
|
|
v = atoiNoCap( s );
|
|
} else {
|
|
v = LookupSymbol( s );
|
|
}
|
|
|
|
// parse add / subtract offsets
|
|
int j;
|
|
while ( token[i] ) {
|
|
for ( j = i + 1 ; j < MAX_LINE_LENGTH ; j++ ) {
|
|
if ( token[j] == '+' || token[j] == '-' || token[j] == 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
v += atoi( &token[i] );
|
|
i = j;
|
|
}
|
|
|
|
return v;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
/*
|
|
BIG HACK: I want to put all 32 bit values in the data
|
|
segment so they can be byte swapped, and all char data in the lit
|
|
segment, but switch jump tables are emited in the lit segment and
|
|
initialized strng variables are put in the data segment.
|
|
|
|
I can change segments here, but I also need to fixup the
|
|
label that was just defined
|
|
|
|
Note that the lit segment is read-write in the VM, so strings
|
|
aren't read only as in some architectures.
|
|
*/
|
|
static void HackToSegment( segmentName_t seg )
|
|
{
|
|
if ( currentSegment == &segment[seg] ) {
|
|
return;
|
|
}
|
|
|
|
currentSegment = &segment[seg];
|
|
if ( passNumber == 0 ) {
|
|
lastSymbol->segment = currentSegment;
|
|
lastSymbol->value = currentSegment->imageUsed;
|
|
}
|
|
}
|
|
|
|
|
|
#define ASM(O) static qboolean TryAssemble##O()
|
|
|
|
// call instructions reset currentArgOffset
|
|
ASM(CALL)
|
|
{
|
|
if ( !strncmp( token, "CALL", 4 ) ) {
|
|
EmitByte( &segment[CODESEG], OP_CALL );
|
|
instructionCount++;
|
|
currentArgOffset = 0;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// arg is converted to a reversed store
|
|
ASM(ARG)
|
|
{
|
|
if ( !strncmp( token, "ARG", 3 ) ) {
|
|
EmitByte( &segment[CODESEG], OP_ARG );
|
|
instructionCount++;
|
|
if ( 8 + currentArgOffset >= 256 ) {
|
|
CodeError( "currentArgOffset >= 256" );
|
|
return qtrue;
|
|
}
|
|
EmitByte( &segment[CODESEG], 8 + currentArgOffset );
|
|
currentArgOffset += 4;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// ret just leaves something on the op stack
|
|
ASM(RET)
|
|
{
|
|
if ( !strncmp( token, "RET", 3 ) ) {
|
|
EmitByte( &segment[CODESEG], OP_LEAVE );
|
|
instructionCount++;
|
|
EmitInt( &segment[CODESEG], 8 + currentLocals + currentArgs );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// pop is needed to discard the return value of a function
|
|
ASM(POP)
|
|
{
|
|
if ( !strncmp( token, "pop", 3 ) ) {
|
|
EmitByte( &segment[CODESEG], OP_POP );
|
|
instructionCount++;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// address of a parameter is converted to OP_LOCAL
|
|
ASM(ADDRF)
|
|
{
|
|
if ( !strncmp( token, "ADDRF", 5 ) ) {
|
|
instructionCount++;
|
|
GetToken();
|
|
int v = ParseExpression() + 16 + currentArgs + currentLocals;
|
|
EmitByte( &segment[CODESEG], OP_LOCAL );
|
|
EmitInt( &segment[CODESEG], v );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// address of a local is converted to OP_LOCAL
|
|
ASM(ADDRL)
|
|
{
|
|
if ( !strncmp( token, "ADDRL", 5 ) ) {
|
|
instructionCount++;
|
|
GetToken();
|
|
int v = ParseExpression() + 8 + currentArgs;
|
|
EmitByte( &segment[CODESEG], OP_LOCAL );
|
|
EmitInt( &segment[CODESEG], v );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(PROC)
|
|
{
|
|
if ( !strcmp( token, "proc" ) ) {
|
|
GetToken(); // function name
|
|
DefineSymbol( token, instructionCount );
|
|
|
|
currentLocals = ParseValue(); // locals
|
|
currentLocals = ( currentLocals + 3 ) & ~3;
|
|
currentArgs = ParseValue(); // arg marshalling
|
|
currentArgs = ( currentArgs + 3 ) & ~3;
|
|
|
|
if ( 8 + currentLocals + currentArgs >= 32767 ) {
|
|
CodeError( "Locals > 32k in %s\n", token );
|
|
}
|
|
|
|
instructionCount++;
|
|
EmitByte( &segment[CODESEG], OP_ENTER );
|
|
EmitInt( &segment[CODESEG], 8 + currentLocals + currentArgs );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(ENDPROC)
|
|
{
|
|
int v, v2;
|
|
if ( !strcmp( token, "endproc" ) ) {
|
|
GetToken(); // skip the function name
|
|
v = ParseValue(); // locals
|
|
v2 = ParseValue(); // arg marshalling
|
|
|
|
// all functions must leave something on the opstack
|
|
instructionCount++;
|
|
EmitByte( &segment[CODESEG], OP_PUSH );
|
|
|
|
instructionCount++;
|
|
EmitByte( &segment[CODESEG], OP_LEAVE );
|
|
EmitInt( &segment[CODESEG], 8 + currentLocals + currentArgs );
|
|
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(ADDRESS)
|
|
{
|
|
if ( !strcmp( token, "address" ) ) {
|
|
GetToken();
|
|
int v = ParseExpression();
|
|
// addresses are 32 bits wide, and therefore go into data segment
|
|
HackToSegment( DATASEG );
|
|
EmitInt( currentSegment, v );
|
|
if ( passNumber == 1 && token[ 0 ] == '$' ) // crude test for labels
|
|
EmitInt( &segment[ JTRGSEG ], v );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(EXPORT)
|
|
{
|
|
if ( !strcmp( token, "export" ) ) {
|
|
GetToken(); // function name
|
|
ExportSymbol( token );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(IMPORT)
|
|
{
|
|
if ( !strcmp( token, "import" ) ) {
|
|
GetToken(); // function name
|
|
ImportSymbol( token );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(CODE)
|
|
{
|
|
if ( !strcmp( token, "code" ) ) {
|
|
currentSegment = &segment[CODESEG];
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(BSS)
|
|
{
|
|
if ( !strcmp( token, "bss" ) ) {
|
|
currentSegment = &segment[BSSSEG];
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(DATA)
|
|
{
|
|
if ( !strcmp( token, "data" ) ) {
|
|
currentSegment = &segment[DATASEG];
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(LIT)
|
|
{
|
|
if ( !strcmp( token, "lit" ) ) {
|
|
currentSegment = &segment[LITSEG];
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(LINE)
|
|
{
|
|
if ( !strcmp( token, "line" ) ) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(FILE)
|
|
{
|
|
if ( !strcmp( token, "file" ) ) {
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(EQU)
|
|
{
|
|
char name[1024];
|
|
if ( !strcmp( token, "equ" ) ) {
|
|
GetToken();
|
|
strcpy( name, token );
|
|
GetToken();
|
|
ExportSymbol( name );
|
|
DefineSymbol( name, atoiNoCap(token) );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(ALIGN)
|
|
{
|
|
if ( !strcmp( token, "align" ) ) {
|
|
int v = ParseValue();
|
|
currentSegment->imageUsed = (currentSegment->imageUsed + v - 1 ) & ~( v - 1 );
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(SKIP)
|
|
{
|
|
if ( !strcmp( token, "skip" ) ) {
|
|
int v = ParseValue();
|
|
currentSegment->imageUsed += v;
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
ASM(BYTE)
|
|
{
|
|
if ( !strcmp( token, "byte" ) ) {
|
|
int c = ParseValue();
|
|
int v = ParseValue();
|
|
|
|
if ( c == 1 ) {
|
|
// character (1-byte) values go into lit(eral) segment
|
|
HackToSegment( LITSEG );
|
|
} else if ( c == 4 ) {
|
|
// 32-bit (4-byte) values go into data segment
|
|
HackToSegment( DATASEG );
|
|
} else {
|
|
CodeError( "%i-byte initialized data not supported", c );
|
|
return qtrue;
|
|
}
|
|
|
|
// emit little endien
|
|
while (c--) {
|
|
EmitByte( currentSegment, (v & 0xFF) ); // paranoid ANDing
|
|
v >>= 8;
|
|
}
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
// code labels are emitted as instruction counts, not byte offsets,
|
|
// because the physical size of the code will change with
|
|
// different run time compilers and we want to minimize the
|
|
// size of the required translation table
|
|
ASM(LABEL)
|
|
{
|
|
if ( !strncmp( token, "LABEL", 5 ) ) {
|
|
GetToken();
|
|
if ( currentSegment == &segment[CODESEG] ) {
|
|
DefineSymbol( token, instructionCount );
|
|
} else {
|
|
DefineSymbol( token, currentSegment->imageUsed );
|
|
}
|
|
return qtrue;
|
|
}
|
|
return qfalse;
|
|
}
|
|
|
|
#undef ASM
|
|
|
|
|
|
static void AssembleLine()
|
|
{
|
|
GetToken();
|
|
if ( !token[0] )
|
|
return;
|
|
|
|
OpTable::const_iterator it = aOpTable.find( token );
|
|
|
|
if (it != aOpTable.end()) {
|
|
int opcode = (*it).second;
|
|
|
|
if ( opcode == OP_UNDEF ) {
|
|
CodeError( "Undefined opcode: %s\n", token );
|
|
}
|
|
|
|
if ( opcode == OP_IGNORE ) {
|
|
return; // we ignore most conversions
|
|
}
|
|
|
|
// sign extensions need to check next parm
|
|
if ( opcode == OP_SEX8 ) {
|
|
GetToken();
|
|
if ( token[0] == '1' ) {
|
|
opcode = OP_SEX8;
|
|
} else if ( token[0] == '2' ) {
|
|
opcode = OP_SEX16;
|
|
} else {
|
|
CodeError( "Bad sign extension: %s\n", token );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// check for expression
|
|
GetToken();
|
|
|
|
EmitByte( &segment[CODESEG], opcode );
|
|
if ( token[0] && opcode != OP_CVIF && opcode != OP_CVFI ) {
|
|
int expression = ParseExpression();
|
|
// code like this can generate non-dword block copies:
|
|
// auto char buf[2] = " ";
|
|
// we are just going to round up. This might conceivably
|
|
// be incorrect if other initialized chars follow.
|
|
if ( opcode == OP_BLOCK_COPY ) {
|
|
expression = ( expression + 3 ) & ~3;
|
|
}
|
|
EmitInt( &segment[CODESEG], expression );
|
|
}
|
|
|
|
instructionCount++;
|
|
return;
|
|
}
|
|
|
|
// these should ideally be sorted by descending statistical frequency
|
|
// tho the difference is ms-trivial, so don't obsess over it
|
|
//#define ASM(O) if (TryAssemble##O()) { printf("STAT "#O"\n"); return; }
|
|
|
|
#define ASM(O) if (TryAssemble##O()) { return; }
|
|
|
|
ASM(BYTE)
|
|
ASM(ADDRL)
|
|
ASM(LINE)
|
|
ASM(ARG)
|
|
ASM(IMPORT)
|
|
ASM(LABEL)
|
|
ASM(ADDRF)
|
|
ASM(CALL)
|
|
ASM(POP)
|
|
ASM(RET)
|
|
ASM(ALIGN)
|
|
ASM(EXPORT)
|
|
ASM(PROC)
|
|
ASM(ENDPROC)
|
|
ASM(ADDRESS)
|
|
ASM(SKIP)
|
|
ASM(EQU)
|
|
ASM(CODE)
|
|
ASM(LIT)
|
|
ASM(FILE)
|
|
ASM(BSS)
|
|
ASM(DATA)
|
|
|
|
#undef ASM
|
|
|
|
CodeError( "Unknown token: %s\n", token );
|
|
}
|
|
|
|
|
|
static void InitTables()
|
|
{
|
|
struct SourceOp {
|
|
const char* name;
|
|
int opcode;
|
|
};
|
|
|
|
static const SourceOp aSourceOps[] = {
|
|
#include "opstrings.h"
|
|
};
|
|
|
|
for (int i = 0; aSourceOps[i].name; ++i) {
|
|
aOpTable[ aSourceOps[i].name ] = aSourceOps[i].opcode;
|
|
}
|
|
|
|
segment[CODESEG].name = "CS";
|
|
segment[DATASEG].name = "DS";
|
|
segment[LITSEG].name = "LS";
|
|
segment[BSSSEG].name = "BS";
|
|
segment[JTRGSEG].name = "JT";
|
|
}
|
|
|
|
|
|
static void ShowTable( const SymTable& table, const char* name )
|
|
{
|
|
report( "%s: %d symbols\n", name, table.size() );
|
|
}
|
|
|
|
|
|
static void WriteMapFile()
|
|
{
|
|
char imageName[MAX_OSPATH];
|
|
COM_StripExtension( outputFilename, imageName, sizeof(imageName) );
|
|
strcat( imageName, ".map" );
|
|
|
|
report( "Writing %s...\n", imageName );
|
|
|
|
FILE* f = SafeOpenWrite( imageName );
|
|
for (int seg = CODESEG; seg <= BSSSEG; ++seg) {
|
|
for (SymTable::const_iterator it = aSymGlobal.begin(); it != aSymGlobal.end(); ++it) {
|
|
const symbol_t* s = (*it).second;
|
|
if ( &segment[seg] == s->segment ) {
|
|
fprintf( f, "%d %8X %s\n", seg, s->value, (*it).first );
|
|
}
|
|
}
|
|
}
|
|
fclose( f );
|
|
}
|
|
|
|
|
|
static void WriteVmFile()
|
|
{
|
|
report( "%i total errors\n", errorCount );
|
|
|
|
char imageName[MAX_OSPATH];
|
|
COM_StripExtension( outputFilename, imageName, sizeof(imageName) );
|
|
strcat( imageName, ".qvm" );
|
|
|
|
remove( imageName );
|
|
|
|
for (int seg = 0; seg < NUM_SEGMENTS; ++seg)
|
|
report( "%s: %7i\n", segment[seg].name, segment[seg].imageUsed );
|
|
report( "instruction count: %i\n", instructionCount );
|
|
|
|
if ( errorCount != 0 ) {
|
|
report( "Not writing a file due to errors\n" );
|
|
return;
|
|
}
|
|
|
|
vmHeader_t header;
|
|
header.vmMagic = VM_MAGIC;
|
|
// Don't write the VM_MAGIC_VER2 bits when maintaining 1.32b compatibility.
|
|
// (I know this isn't strictly correct due to padding, but then platforms
|
|
// that pad wouldn't be able to write a correct header anyway).
|
|
// Note: if vmHeader_t changes, this needs to be adjusted too.
|
|
const int headerSize = sizeof( header );
|
|
|
|
header.instructionCount = instructionCount;
|
|
header.codeOffset = headerSize;
|
|
header.codeLength = segment[CODESEG].imageUsed;
|
|
header.dataOffset = header.codeOffset + segment[CODESEG].imageUsed;
|
|
header.dataLength = segment[DATASEG].imageUsed;
|
|
header.litLength = segment[LITSEG].imageUsed;
|
|
header.bssLength = segment[BSSSEG].imageUsed;
|
|
|
|
report( "Writing to %s\n", imageName );
|
|
|
|
CreatePath( imageName );
|
|
FILE* f = SafeOpenWrite( imageName );
|
|
SafeWrite( f, &header, headerSize );
|
|
SafeWrite( f, &segment[CODESEG].image, segment[CODESEG].imageUsed );
|
|
SafeWrite( f, &segment[DATASEG].image, segment[DATASEG].imageUsed );
|
|
SafeWrite( f, &segment[LITSEG].image, segment[LITSEG].imageUsed );
|
|
fclose( f );
|
|
}
|
|
|
|
|
|
static void Assemble()
|
|
{
|
|
int i;
|
|
|
|
report( "outputFilename: %s\n", outputFilename );
|
|
|
|
for ( i = 0 ; i < numAsmFiles ; i++ ) {
|
|
char filename[MAX_OSPATH];
|
|
strcpy( filename, asmFileNames[ i ] );
|
|
COM_DefaultExtension( filename, sizeof(filename), ".asm" );
|
|
LoadFile( filename, (void **)&asmFiles[i] );
|
|
}
|
|
|
|
// assemble
|
|
for ( passNumber = 0 ; passNumber < 2 ; passNumber++ ) {
|
|
segment[LITSEG].segmentBase = segment[DATASEG].imageUsed;
|
|
segment[BSSSEG].segmentBase = segment[LITSEG].segmentBase + segment[LITSEG].imageUsed;
|
|
segment[JTRGSEG].segmentBase = segment[BSSSEG].segmentBase + segment[BSSSEG].imageUsed;
|
|
for ( i = 0 ; i < NUM_SEGMENTS ; i++ ) {
|
|
segment[i].imageUsed = 0;
|
|
}
|
|
segment[DATASEG].imageUsed = 4; // skip the 0 byte, so NULL pointers are fixed up properly
|
|
instructionCount = 0;
|
|
|
|
for ( i = 0 ; i < numAsmFiles ; i++ ) {
|
|
currentFileIndex = i;
|
|
currentFileName = asmFileNames[ i ];
|
|
currentFileLine = 0;
|
|
report( "pass %i: %s\n", passNumber, currentFileName );
|
|
fflush( NULL );
|
|
const char* p = asmFiles[i];
|
|
while ( p ) {
|
|
p = ExtractLine( p );
|
|
AssembleLine();
|
|
}
|
|
//ShowTable( aSymLocal[i], "Locals" );
|
|
}
|
|
|
|
// align all the segments
|
|
for ( i = 0 ; i < NUM_SEGMENTS ; i++ ) {
|
|
segment[i].imageUsed = (segment[i].imageUsed + 3) & ~3;
|
|
}
|
|
}
|
|
|
|
SymTable::const_iterator it = aSymGlobal.find( "vmMain" );
|
|
if ( (it == aSymGlobal.end()) || ((*it).second->segment != &segment[CODESEG]) || (*it).second->value ) {
|
|
CodeError( "vmMain missing or not the first symbol in the QVM\n" );
|
|
}
|
|
|
|
// reserve the stack in bss
|
|
const int stackSize = 0x10000;
|
|
DefineSymbol( "_stackStart", segment[BSSSEG].imageUsed );
|
|
segment[BSSSEG].imageUsed += stackSize;
|
|
DefineSymbol( "_stackEnd", segment[BSSSEG].imageUsed );
|
|
|
|
WriteVmFile();
|
|
|
|
// only write the map file if there were no errors
|
|
if ( options.writeMapFile && !errorCount ) {
|
|
WriteMapFile();
|
|
}
|
|
}
|
|
|
|
|
|
static void ParseOptionFile( const char* filename )
|
|
{
|
|
char expanded[MAX_OSPATH];
|
|
strcpy( expanded, filename );
|
|
COM_DefaultExtension( expanded, sizeof(expanded), ".q3asm" );
|
|
|
|
char* text;
|
|
LoadFile( expanded, (void **)&text );
|
|
if ( !text ) {
|
|
return;
|
|
}
|
|
|
|
char* text_p = text;
|
|
|
|
while( ( text_p = ASM_Parse( text_p ) ) != 0 ) {
|
|
if ( !strcmp( com_token, "-o" ) ) {
|
|
// allow output override in option file
|
|
text_p = ASM_Parse( text_p );
|
|
if ( text_p ) {
|
|
strcpy( outputFilename, com_token );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
asmFileNames[ numAsmFiles ] = copystring( com_token );
|
|
numAsmFiles++;
|
|
}
|
|
}
|
|
|
|
|
|
int main( int argc, char **argv )
|
|
{
|
|
if ( argc < 2 ) {
|
|
Com_Error( ERR_FATAL, "Usage: %s [OPTION]... [FILES]...\n"
|
|
"Assemble LCC bytecode assembly to Q3VM bytecode.\n\n"
|
|
"-o OUTPUT Write assembled output to file OUTPUT.qvm\n"
|
|
"-f LISTFILE Read options and list of files to assemble from LISTFILE\n"
|
|
"-m Write symbols to a .map file\n"
|
|
"-v Verbose compilation report\n"
|
|
, argv[0] );
|
|
}
|
|
|
|
float tStart = Q_FloatTime();
|
|
|
|
// default filename is "q3asm"
|
|
strcpy( outputFilename, "q3asm" );
|
|
numAsmFiles = 0;
|
|
|
|
int i;
|
|
for (i = 1; i < argc; ++i) {
|
|
if ( argv[i][0] != '-' ) {
|
|
break;
|
|
}
|
|
|
|
if ( !strcmp( argv[i], "-o" ) ) {
|
|
if ( i == argc - 1 ) {
|
|
Com_Error( ERR_FATAL, "-o requires a filename" );
|
|
}
|
|
strcpy( outputFilename, argv[++i] );
|
|
continue;
|
|
}
|
|
|
|
if ( !strcmp( argv[i], "-f" ) ) {
|
|
if ( i == argc - 1 ) {
|
|
Com_Error( ERR_FATAL, "-f requires a filename" );
|
|
}
|
|
ParseOptionFile( argv[++i] );
|
|
continue;
|
|
}
|
|
|
|
// by default (no -v option), q3asm remains silent except for critical errors
|
|
// verbosity turns on all messages, error or not
|
|
if ( !strcmp( argv[ i ], "-v" ) ) {
|
|
options.verbose = qtrue;
|
|
continue;
|
|
}
|
|
|
|
if ( !strcmp( argv[ i ], "-m" ) ) {
|
|
options.writeMapFile = qtrue;
|
|
continue;
|
|
}
|
|
|
|
Com_Error( ERR_FATAL, "Unknown option: %s", argv[i] );
|
|
}
|
|
|
|
// the rest of the command line args are asm files
|
|
for (; i < argc; ++i) {
|
|
asmFileNames[ numAsmFiles ] = copystring( argv[ i ] );
|
|
numAsmFiles++;
|
|
}
|
|
|
|
#if defined(_DEBUG)
|
|
options.writeMapFile = qtrue;
|
|
options.verbose = qtrue;
|
|
#endif
|
|
|
|
InitTables();
|
|
Assemble();
|
|
|
|
report( "%s compiled in %.3fs\n", outputFilename, Q_FloatTime() - tStart );
|
|
ShowTable( aSymImported, "Imported" );
|
|
ShowTable( aSymGlobal, "Globals" );
|
|
|
|
return errorCount;
|
|
}
|
|
|