0,0 → 1,765 |
; This program parses .fas file generated by fasm |
; and prints the list of dependencies for make. |
; Usage: fasmdep [-e] [<input file> [<output file>]]. |
; If input file is not given, the program reads from stdin. |
; If output file is not given, the program writes to stdout. |
; If the option -e is given, the program also creates an empty |
; goal for every dependency, this prevents make errors when |
; files are renamed. |
|
; This definition controls the choice of platform-specific code. |
;define OS WINDOWS |
define OS LINUX |
|
; Program header. |
match =WINDOWS,OS { include 'windows_header.inc' } |
match =LINUX,OS { include 'linux_header.inc' } |
|
include 'fas.inc' |
|
; Main code |
start: |
; 1. Setup stack frame, zero-initialize all local variables. |
virtual at ebp - .localsize |
.begin: |
.flags dd ? ; 1 if '-e' is given |
.in dd ? ; handle of input file |
.out dd ? ; handle of output file |
.buf dd ? ; pointer to data of .fas file |
.allocated dd ? ; number of bytes allocated for .buf |
.free dd ? ; number of bytes free in .buf |
.outstart dd ? ; offset in .buf for start of data to output |
.names dd ? ; offset in .buf for start of output names |
.include dd ? ; offset in .buf to value of %INCLUDE% |
.testname dd ? ; offset in .buf for start of current file name |
.prevfile dd ? ; offset in .buf for previous included file name |
.prevfilefrom dd ? ; offset in .buf for .asm/.inc for .prevfile |
.localsize = $ - .begin |
match =LINUX,OS { |
.argc dd ? |
.argv: |
} |
end virtual |
|
mov ebp, esp |
xor eax, eax |
repeat .localsize / 4 |
push eax |
end repeat |
; 2. Call the parser of the command line. |
call get_params |
; 3. Load the input file. |
; Note that stdin can be a pipe, |
; useful in bash-construction "fasm input.asm -s >(fasmdep > input.Po)", |
; so its size is not always known in the beginning. |
; So, we must read input file in portions, reallocating memory if needed. |
.readloop: |
; 3a. If the size is less than 32768 bytes, reallocate buffer. |
; Otherwise, goto 3c. |
cmp [.free], 32768 |
jae .norealloc |
; 3b. Reallocate buffer. |
; Start with 65536 bytes and then double the size every time. |
mov eax, 65536 |
mov ecx, [.allocated] |
add ecx, ecx |
jz @f |
mov eax, ecx |
@@: |
call realloc |
.norealloc: |
; 3c. Read the next portion. |
call read |
sub [.free], eax |
test eax, eax |
jnz .readloop |
; 4. Sanity checks. |
; We don't use .section_* and .symref_*, so allow them to be absent. |
mov edi, [.allocated] |
sub edi, [.free] |
; Note that edi = number of bytes which were read |
; and simultaneously pointer to free space in the buffer relative to [.buf]. |
cmp edi, fas_header.section_offs |
jb badfile |
mov ebx, [.buf] |
cmp [ebx+fas_header.signature], FAS_SIGNATURE |
jnz badfile |
cmp [ebx+fas_header.headersize], fas_header.section_offs |
jb badfile |
; 5. Get %INCLUDE% environment variable, it will be useful. |
mov [.include], edi |
mov esi, include_variable |
sub esi, ebx |
call get_environment_variable |
; 6. Start writing dependencies: copy output and input files. |
mov [.outstart], edi |
; 6a. Copy output name. |
mov esi, [ebx+fas_header.output] |
call copy_asciiz_escaped |
; 6b. Write ": ". |
stdcall alloc_in_buf, 2 |
mov word [edi+ebx], ': ' |
inc edi |
inc edi |
; 6c. Copy input name. |
mov [.names], edi |
mov esi, [ebx+fas_header.input] |
call copy_asciiz_escaped |
; 7. Scan for 'include' dependencies. |
; 7a. Get range for scanning. |
mov edx, [ebx+fas_header.preproc_size] |
mov esi, [ebx+fas_header.preproc_offs] |
add edx, esi |
.include_loop: |
; 7b. Loop over preprocessed lines in the range. |
cmp esi, edx |
jae .include_done |
; 7c. For every line, ignore header and do internal loop over tokens. |
add esi, preproc_line_header.contents |
.include_loop_int: |
; There are five types of tokens: |
; 1) "start preprocessor data" with code ';' <byte length> <length*byte token> |
; 2) "quoted string" with code '"' <dword length> <length*byte string> |
; 3) "word" with code 1Ah <byte length> <length*byte word> |
; 4) one-byte tokens like "+", "(" and so on |
; 5) "end-of-line" token with code 0. |
mov al, [esi+ebx] |
inc esi |
; 7d. First, check token type parsing the first byte. |
; For preprocessor tokens, continue to 7e. |
; For quoted strings, go to 7g. |
; For words, go to 7h. |
; For end-of-line, exit the internal loop, continue the external loop. |
; Otherwise, just continue the internal loop. |
cmp al, ';' |
jnz .notprep |
; 7e. For "include" tokens length=7, token is "include", the next token is |
; quoted string. |
; These tokens are handled in 7f, other preprocessor tokens in 7g. |
cmp byte [esi+ebx], 7 |
jnz .notinclude |
cmp dword [esi+ebx+1], 'incl' |
jnz .notinclude |
cmp dword [esi+ebx+5], 'ude"' |
jnz .notinclude |
; 7f. Skip over end of this token and the type byte of the next token; |
; this gives the pointer to length-prefixed string, which should be added |
; to dependencies. Note that doing that skips that string, |
; so after copying just continue the internal loop. |
add esi, 9 |
call add_separator |
call copy_name_escaped |
jmp .include_loop_int |
.quoted: |
; 7g. To skip a word, load the dword length and add it to pointer. |
add esi, [esi+ebx] |
add esi, 4 |
jmp .include_loop_int |
.notinclude: |
; 7h. To skip this token, load the byte length and add it to pointer. |
; Note that word tokens and preprocessor tokens have a similar structure, |
; so both are handled here. |
movzx eax, byte [esi+ebx] |
lea esi, [esi+eax+1] |
jmp .include_loop_int |
.notprep: |
cmp al, 1Ah |
jz .notinclude |
cmp al, '"' |
jz .quoted |
test al, al |
jnz .include_loop_int |
jmp .include_loop |
.include_done: |
; 8. Scan for 'file' dependencies. |
; 8a. Get range for scanning. |
; Note that fas_header.asm_size can be slighly greater than the |
; real size, so we break from the loop when there is no space left |
; for the entire asm_row structure, not when .asm_size is exactly reached. |
mov esi, [ebx+fas_header.asm_offs] |
mov edx, [ebx+fas_header.asm_size] |
add edx, esi |
sub edx, asm_row.sizeof |
push edx |
jc .file_done |
.file_loop: |
; 8b. Loop over assembled lines in the range. |
cmp esi, [esp] |
ja .file_done |
; 8c. For every assembled line, look at first token; |
; go to 8d for token 'file', |
; go to 8e for token 'format', |
; go to 8f for token 'section', |
; continue the loop otherwise. |
mov eax, [esi+ebx+asm_row.preproc_offs] |
add eax, [ebx+fas_header.preproc_offs] |
cmp byte [eax+ebx+preproc_line_header.contents], 1Ah |
jnz .file_next |
movzx ecx, byte [eax+ebx+preproc_line_header.contents+1] |
cmp cl, 4 |
jnz .file_no_file |
cmp dword [eax+ebx+preproc_line_header.contents+2], 'file' |
jnz .file_no_file4 |
; 8d. For lines starting with 'file' token, loop over tokens and get names. |
; Note that there can be several names in one line. |
; Parsing of tokens is similar to step 5 with the difference that |
; preprocessor token stops processing: 'file' directives are processed |
; in assembler stage. |
; We save/restore esi since it is used for another thing in the internal loop; |
; we push eax, which currently contains ebx-relative pointer to |
; preproc_line_header, to be able to access it from resolve_name. |
push esi eax |
lea esi, [eax+preproc_line_header.contents+6] |
.file_loop_int: |
mov al, [esi+ebx] |
inc esi |
test al, al |
jz .file_loop_int_done |
cmp al, ';' |
jz .file_loop_int_done |
cmp al, 1Ah |
jz .fileword |
cmp al, '"' |
jnz .file_loop_int |
call resolve_name |
add esi, [esi+ebx-4] |
jmp .file_loop_int |
.fileword: |
movzx eax, byte [esi+ebx] |
lea esi, [esi+eax+1] |
jmp .file_loop_int |
.file_loop_int_done: |
pop eax esi |
jmp .file_next |
.file_no_file4: |
cmp dword [eax+ebx+preproc_line_header.contents+2], 'data' |
jnz .file_next |
jmp .file_scan_from |
.file_no_file: |
cmp cl, 6 |
jnz .file_no_format |
cmp dword [eax+ebx+preproc_line_header.contents+2], 'form' |
jnz .file_no_format |
cmp word [eax+ebx+preproc_line_header.contents+6], 'at' |
jnz .file_no_format |
; 8e. For lines starting with 'format' token, loop over tokens and look for stub name. |
; Note that there can be another quoted string, namely file extension; |
; stub name always follows the token 'on'. |
mov edx, TokenOn |
call scan_after_word |
jmp .file_next |
.file_no_format: |
cmp cl, 7 |
jnz .file_no_section |
cmp dword [eax+ebx+preproc_line_header.contents+2], 'sect' |
jnz .file_no_section |
mov edx, dword [eax+ebx+preproc_line_header.contents+6] |
and edx, 0FFFFFFh |
cmp edx, 'ion' |
jnz .file_no_section |
.file_scan_from: |
mov edx, TokenFrom |
call scan_after_word |
.file_no_section: |
.file_next: |
add esi, asm_row.sizeof |
jmp .file_loop |
.file_done: |
pop edx |
; 9. Write result. |
; 9a. Append two newlines to the end of buffer. |
stdcall alloc_in_buf, 2 |
mov word [edi+ebx], 10 * 101h |
inc edi |
inc edi |
; 9b. If '-e' option was given, duplicate dependencies list as fake goals |
; = copy all of them and append ":\n\n". |
cmp [.flags], 0 |
jz .nodup |
lea ecx, [edi+1] |
mov esi, [.names] |
sub ecx, esi |
stdcall alloc_in_buf, ecx |
add esi, ebx |
add edi, ebx |
rep movsb |
mov byte [edi-3], ':' |
mov word [edi-2], 10 * 101h |
sub edi, ebx |
.nodup: |
; 9c. Write to output file. |
mov eax, [.outstart] |
sub edi, eax |
add eax, ebx |
call write |
; 10. Exit. |
xor eax, eax |
call exit |
|
; Helper procedure for steps 8e and 8f of main algorithm. |
; Looks for quoted strings after given word in edx. |
scan_after_word: |
push esi eax |
lea esi, [eax+preproc_line_header.contents+2+ecx] |
.loop: |
xor ecx, ecx |
.loop_afterword: |
mov al, [esi+ebx] |
inc esi |
test al, al |
jz .loop_done |
cmp al, ';' |
jz .loop_done |
cmp al, 1Ah |
jz .word |
cmp al, '"' |
jnz .loop |
test ecx, ecx |
jz .skip_quoted |
call resolve_name |
.loop_done: |
pop eax esi |
ret |
.skip_quoted: |
add esi, [esi+ebx] |
add esi, 4 |
jmp .loop |
.word: |
movzx ecx, byte [esi+ebx] |
lea esi, [esi+ecx+1] |
cmp cl, byte [edx] |
jnz .loop |
push esi edi |
add esi, ebx |
sub esi, ecx |
lea edi, [edx+1] |
repz cmpsb |
pop edi esi |
jnz .loop |
dec ecx |
jmp .loop_afterword |
|
; Helper procedure for step 6 of the main procedure. |
; Copies the ASCIIZ name from strings section to the buffer. |
copy_asciiz_escaped: |
add esi, [ebx+fas_header.strings_offs] |
.loop: |
mov al, [esi+ebx] |
test al, al |
jz .nothing |
call copy_char_escaped |
jmp .loop |
.nothing: |
ret |
|
; Helper procedure for step 7 of the main procedure. |
; Copies the name with known length to the buffer. |
copy_name_escaped: |
mov ecx, [esi+ebx] |
add esi, 4 |
test ecx, ecx |
jz .nothing |
push ecx |
.loop: |
mov al, [esi+ebx] |
call copy_char_escaped |
dec dword [esp] |
jnz .loop |
pop ecx |
.nothing: |
ret |
|
; Helper procedure for steps 7 and 8 of the main procedure. |
; Writes separator of file names in output = " \\\n". |
add_separator: |
stdcall alloc_in_buf, 3 |
mov word [edi+ebx], ' \' |
mov byte [edi+ebx+2], 10 |
add edi, 3 |
ret |
|
; Helper procedure for step 7 of the main procedure. |
; Resolves the path to 'file' dependency and copies |
; the full name to the buffer. |
resolve_name: |
; FASM uses the following order to search for referenced files: |
; * path of currently assembling file, which may be .asm or .inc |
; * paths from %INCLUDE% for versions >= 1.70 |
; * current directory = file name is taken as is, without prepending dir name |
; We mirror this behaviour, trying to find an existing file somewhere. |
; There can be following reasons for the file can not be found anywhere: |
; * it has been deleted between compilation and our actions |
; * it didn't exist at all, compilation has failed |
; * we are running in environment different from fasm environment. |
; Assume that the last reason is most probable and that invalid dependency |
; is better than absent dependency (it is easier to fix an explicit error |
; than a silent one) and output file name without prepending dir name, |
; as in the last case. Actually, we even don't need to test existence |
; of the file in the current directory. |
add esi, 4 ; skip string length |
; 1. Get ebx-relative pointer to preproc_line_header, see the comment in start.7d |
mov eax, [esp+4] |
; 2. Get the path to currently processing file. |
push esi |
.getpath: |
test byte [eax+ebx+preproc_line_header.line_number+3], 80h |
jz @f |
mov eax, [eax+ebx+preproc_line_header.line_offset] |
add eax, [ebx+fas_header.preproc_offs] |
jmp .getpath |
@@: |
mov edx, [eax+ebx+preproc_line_header.source_name] |
test edx, edx |
jz .frommain |
add edx, [ebx+fas_header.preproc_offs] |
jmp @f |
.frommain: |
mov edx, [ebx+fas_header.input] |
add edx, [ebx+fas_header.strings_offs] |
@@: |
; 3. Check that it is not a duplicate of the previous dependency. |
; 3a. Compare preprocessor units. |
cmp edx, [start.prevfilefrom] |
jnz .nodup |
; 3b. Compare string lengths. |
mov eax, [start.prevfile] |
mov ecx, [eax+ebx-4] |
cmp ecx, [esi+ebx-4] |
jnz .nodup |
; 3c. Compare string contents. |
push esi edi |
lea edi, [eax+ebx] |
add esi, ebx |
rep cmpsb |
pop edi esi |
jnz .nodup |
; 3d. It is duplicate, just return. |
pop esi |
ret |
.nodup: |
; 3e. It is not duplicate. Output separator. |
mov [start.prevfilefrom], edx |
mov [start.prevfile], esi |
call add_separator |
; 4. Cut the last component of the path found in step 2. |
mov ecx, edx |
mov esi, edx |
.scanpath: |
mov al, [edx+ebx] |
test al, al |
jz .scandone |
cmp al, '/' |
jz .slash |
cmp al, '\' |
jnz .scannext |
.slash: |
lea ecx, [edx+1] |
.scannext: |
inc edx |
jmp .scanpath |
.scandone: |
sub ecx, esi |
; 5. Try path found in step 4. If found, go to step 8. |
mov [start.testname], edi |
stdcall copy_string, esi, ecx |
pop esi |
call expand_environment |
call test_file_exists |
test eax, eax |
jns .found |
call revert_testname |
; 6. Try each of include paths. For every path, if file is found, go to step 8. |
; Otherwise, continue loop over include path. |
; Skip this step before 1.70. |
cmp [ebx+fas_header.major], 1 |
ja .checkenv |
jb .nocheckenv |
cmp [ebx+fas_header.minor], 70 |
jb .nocheckenv |
.checkenv: |
mov ecx, [start.include] |
.includeloop_ext: |
mov eax, ecx |
.includeloop_int: |
cmp byte [eax+ebx], 0 |
jz @f |
cmp byte [eax+ebx], ';' |
jz @f |
inc eax |
jmp .includeloop_int |
@@: |
push eax |
sub eax, ecx |
jz @f |
stdcall copy_string, ecx, eax |
cmp byte [edi+ebx-1], '/' |
jz .hasslash |
@@: |
stdcall alloc_in_buf, 1 |
mov byte [edi+ebx], '/' |
inc edi |
.hasslash: |
call expand_environment |
call test_file_exists |
pop ecx |
test eax, eax |
jns .found |
call revert_testname |
cmp byte [ecx+ebx], 0 |
jz .notfound |
inc ecx |
cmp byte [ecx+ebx], 0 |
jnz .includeloop_ext |
.nocheckenv: |
.notfound: |
; 7. File not found neither near the current preprocessor unit nor in %INCLUDE%. |
; Assume that it is in the current directory. |
call expand_environment |
.found: |
; 8. Currently we have file name from [start.testname] to edi; |
; it is zero-terminated and not space-escaped. Fix both issues. |
dec edi |
inc [start.free] |
push esi |
mov edx, [start.testname] |
.escapeloop: |
cmp edx, edi |
jae .escapedone |
cmp byte [edx+ebx], ' ' |
jnz .noescape |
stdcall alloc_in_buf, 1 |
mov ecx, edi |
sub ecx, edx |
push edi |
add edi, ebx |
lea esi, [edi-1] |
std |
rep movsb |
cld |
pop edi |
inc edi |
mov byte [edx+ebx], '\' |
inc edx |
.noescape: |
inc edx |
jmp .escapeloop |
.escapedone: |
pop esi |
ret |
|
; Helper procedure for resolve_name. |
; Allocates space in the buffer and appends the given string to the buffer. |
copy_string: |
mov eax, [esp+8] |
test eax, eax |
jz .nothing |
stdcall alloc_in_buf, eax |
mov ecx, [esp+4] |
.copy: |
mov al, [ecx+ebx] |
inc ecx |
cmp al, '\' |
jnz @f |
mov al, '/' |
@@: |
mov [edi+ebx], al |
inc edi |
dec dword [esp+8] |
jnz .copy |
.nothing: |
ret 8 |
|
; Helper procedure for resolve_name. Undoes appending of last file name. |
revert_testname: |
add [start.free], edi |
mov edi, [start.testname] |
sub [start.free], edi |
ret |
|
; Helper procedure for resolve_name. Copies string from esi to edi, |
; expanding environment variables. |
expand_environment: |
; 1. Save esi to restore it in the end of function. |
push esi |
; 2. Push string length to the stack to be used as a variable. |
pushd [esi+ebx-4] |
; 3. Scan loop. |
.scan: |
; 3a. Scan for '%' sign. |
call find_percent |
.justcopy: |
; 3b. Copy the part from the beginning of current portion to '%' sign, |
; advance pointer to '%' sign, or end-of-string if no '%' found. |
push eax |
sub eax, esi |
stdcall copy_string, esi, eax |
pop esi |
; 3c. If string has ended, break from the loop. |
cmp dword [esp], 0 |
jz .scandone |
; 3d. Advance over '%' sign. |
inc esi |
dec dword [esp] |
; 3e. Find paired '%'. |
call find_percent |
; 3f. If there is no paired '%', just return to 3b and copy remaining data, |
; including skipped '%'; after that, 3c would break from the loop. |
dec esi |
cmp dword [esp], 0 |
jz .justcopy |
; 3g. Otherwise, get the value of environment variable. |
; Since get_environment_variable requires zero-terminated string |
; and returns zero-terminated string, temporarily overwrite trailing '%' |
; and ignore last byte in returned string. |
; Also convert any backslashes to forward slashes. |
inc esi |
mov byte [eax+ebx], 0 |
push eax |
push edi |
call get_environment_variable |
dec edi |
inc [start.free] |
pop eax |
.replaceslash: |
cmp eax, edi |
jz .replaceslash_done |
cmp byte [eax+ebx], '\' |
jnz @f |
mov byte [eax+ebx], '/' |
@@: |
inc eax |
jmp .replaceslash |
.replaceslash_done: |
pop esi |
mov byte [esi+ebx], '%' |
; 3h. Advance over trailing '%'. |
inc esi |
dec dword [esp] |
; 3i. Continue the loop. |
jmp .scan |
.scandone: |
; 4. Zero-terminate resulting string. |
stdcall alloc_in_buf, 1 |
mov byte [edi+ebx], 0 |
inc edi |
; 5. Pop stack variable initialized in step 2. |
pop eax |
; 6. Restore esi saved in step 1 and return. |
pop esi |
ret |
|
; Helper procedure for expand_environment. |
; Scans the string in esi with length [esp+4] |
; until '%' is found or line ended. |
find_percent: |
mov eax, esi |
cmp dword [esp+4], 0 |
jz .nothing |
.scan: |
cmp byte [eax+ebx], '%' |
jz .nothing |
inc eax |
dec dword [esp+4] |
jnz .scan |
.nothing: |
ret |
|
; Helper procedure for copy_{name,asciiz}_escaped. |
; Allocates space and writes one character, possibly escaped. |
copy_char_escaped: |
cmp al, ' ' |
jnz .noescape |
stdcall alloc_in_buf, 1 |
mov byte [edi+ebx], '\' |
inc edi |
.noescape: |
stdcall alloc_in_buf, 1 |
mov al, [esi+ebx] |
inc esi |
cmp al, '\' |
jnz @f |
mov al, '/' |
@@: |
mov [edi+ebx], al |
inc edi |
ret |
|
; Helper procedure for ensuring that there is at least [esp+4] |
; free bytes in the buffer. |
alloc_in_buf: |
mov eax, [esp+4] |
sub [start.free], eax |
jb .need_realloc |
ret 4 |
.need_realloc: |
mov eax, [start.allocated] |
add eax, eax |
push ecx edx |
call realloc |
pop edx ecx |
cmp [start.free], 0 |
jl .need_realloc |
mov ebx, [start.buf] |
ret 4 |
|
badfile: |
mov esi, badfile_string |
call sayerr |
mov al, 1 |
call exit |
|
information: |
mov esi, information_string |
call sayerr |
mov al, 2 |
call exit |
|
nomemory: |
mov esi, nomemory_string |
call sayerr |
mov al, 3 |
call exit |
|
in_openerr: |
mov esi, in_openerr_string |
jmp in_err |
readerr: |
mov esi, readerr_string |
in_err: |
call sayerr |
mov al, 4 |
call exit |
|
out_openerr: |
mov esi, out_openerr_string |
jmp out_err |
writeerr: |
mov esi, writeerr_string |
out_err: |
call sayerr |
mov al, 5 |
call exit |
|
; Platform-specific procedures. |
match =WINDOWS,OS { include 'windows_sys.inc' } |
match =LINUX,OS { include 'linux_sys.inc' } |
|
; Data |
macro string a, [b] { |
common |
db a ## _end - a |
a db b |
a ## _end: |
} |
|
string information_string, 'Usage: fasmdep [-e] [<input.fas> [<output.Po>]]',10 |
string badfile_string, 'Not .fas file',10 |
string nomemory_string, 'No memory',10 |
string in_openerr_string, 'Cannot open input file',10 |
string readerr_string, 'Read error',10 |
string out_openerr_string, 'Cannot create output file',10 |
string writeerr_string, 'Write error',10 |
|
include_variable db 'INCLUDE',0 |
TokenOn db 2,'on' |
TokenFrom db 4,'from' |