cnq3/code/tools/asm/q3asm.cpp

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;
}