This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

asm6 assumes little-endian host [not anymore!]

asm6 assumes little-endian host [not anymore!]
by on (#63472)
I was trying to get asm6 working on my machine, but all the data values were outputting as zero. I finally traced it down to it assuming that the host is little-endian, where it essentially does in many places:
Code:
int i = ...;
write_bytes_to_output( &i, 1 ); // for byte output
write_bytes_to_output( &i, 2 ); // for 16-bit word output

rather than the much more portable
Code:
int i = ...;
unsigned char c = i;
write_bytes_to_output( &c, 1 );
unsigned char word [2] = { i, i >> 8 };
write_bytes_to_output( word, 2 );

by on (#63484)
Doing this for every single write_bytes_to_output() call is wasteful.

The source code should be modified to either:
a) have an appropriate #ifdef to define little or big endian (most people use LITTLE_ENDIAN and BIG_ENDIAN), or,
b) have a detection subroutine that runs *once* that detects+defines endian state and modify write_bytes_to_output() to utilise said state.

(a) is more efficient (and is more common) but requires the user have to rebuild the binary from source if they change architectures (fairly common anyway), while (b) doesn't require this.

Detection is easy:

Code:
#define LITTLE_ENDIAN 0
#define BIG_ENDIAN    1

/* Prototype declaration */
int test_endian(void);

/* Function declaration */
int test_endian(void) {
  int i = 1;
  char *p = (char *)&i;

  if (p[0] == 1) { return LITTLE_ENDIAN; }
  return BIG_ENDIAN;
}


Again, you'd only need to call this once.

by on (#63486)
I found more endian-dependence. In places it does *(short*)char_ptr = '=' for example, instead of the portable strcpy( char_ptr, "=" ). Or similarly, if ( *(short*)char_ptr == '=' instead of !strcmp( char_ptr, "=" ).

And then there are the pointer casts, that I haven't figured out yet. Some of them use pointers as booleans, doing char_ptr = (char*) 1 for true, rather than the portable static char true_sentinel; char_ptr = &true_sentinel.

The twisted nature of these optimizations makes it a kind of interesting project to consider making portable, especially doing so while making minimal modifications.

by on (#63487)
I'm not sure I'd call those "optimisations" (no offence intended, loopy). The most recent examples look like an accident waiting to happen; ouch.

Regarding the pseudo-boolean stuff: depending on how the code works and what its intention is, it might make more overall sense to make it adhere to C99 (which officially offers the "bool" type).

The ASM6 parser isn't really very... well, I guess I shouldn't go there. I think the original point was that loopy wrote it for his own use, release it into the wild for others to use, yadda yadda. This is probably one of those "it works except when it doesn't" situations. :-)

by on (#63488)
Yeah, I partly just wanted to warn anyone considering compiling this on a big-endian machine, and perhaps post a portable version. I was impressed with the simplicity of the memory model. It was clearly made to just assemble and work, without arcane segments and other things used by other assemblers. I mainly just wanted to have it around so I could see how difficult it was to modify code I release to work with it.

The thing with the pointers is that it's also using them as normal pointers. So it's a sort of bool/char* union, but without having to use a union. It may even use this as the type flag, so if the pointer is (char*)1, then it's a bool with the value true. If it's NULL, then it's a bool with the value false. If it's neither, then it's a normal char* pointing at something.

by on (#63491)
Quote:
Again, you'd only need to call this once.


Of course, the problem with such a test function is that it can only be accomplished at run-time. Thus, any time you were to actually use a big or little endian specific function, you'd had to go through a function pointer or conditional test. Or go really evil and rely on self-modifying code :)

Best bet is to try and detect the platform based on compiler-specific #defines, and fall back on letting the user manually choose endianness. And finally, create a run-time assertion on startup to ensure the correct endian was chosen.

Still, for an NES assembler, is it really worth the speed benefit for all the extra hassle; when you can use the same code on all platforms? I can't imagine writing more than 1MB of data this way. Surely the added overhead isn't even close to 1ms.

Quote:
I was impressed with the simplicity of the memory model. It was clearly made to just assemble and work, without arcane segments and other things used by other assemblers.


I've been trying to convince people of that approach for over a decade now. That kind of flexible magic can be there, just only require it when it is really required.

Quote:
So it's a sort of bool/char* union, but without having to use a union. It may even use this as the type flag, so if the pointer is (char*)1, then it's a bool with the value true. If it's NULL, then it's a bool with the value false. If it's neither, then it's a normal char* pointing at something.


So then, I assume a value of (char*)2 represents a file not found condition?

by on (#63492)
byuu wrote:
So then, I assume a value of (char*)2 represents a file not found condition?


I wonder how many people here do not read The Daily WTF.

by on (#63494)
blargg wrote:
Yeah, I partly just wanted to warn anyone considering compiling this on a big-endian machine

Hey Blargg, ANY chance I can get you to try compiling/using PASM on a big-endian machine?

It requires flex/bison. On Windows I use cygwin but it also compiles in Linux and OSX. It compiles both a library (static, included in NESICIDE) and an executable (for me to test it externally from NESICIDE).

It is written to be ASM6 syntax compatible.

Source for it is here:

http://www.gitorious.org/nesicide/nesicide2-master/trees/master/compiler

(the makefile and the files prefixed with pasm_)

by on (#63496)
I would definitely not look to asm6 as an example of good code. In some places, it's quite atrocious. Understand that it was originally written for an audience of one (myself), to be run on exactly one computer. Very much a "just make it work" mentality. At the time, I had no knowledge of how a parser should be written, I was figuring things out as I went along.

blargg wrote:
The twisted nature of these optimizations makes it a kind of interesting project to consider making portable, especially doing so while making minimal modifications.


I'm glad you're finding it interesting, at least.

by on (#63499)
I just want to apologize for somewhat making an example of asm6. I was kind of annoyed that what could have worked fine (no OS-specific crap, for example) was marred by endian-dependence. I understand your goals for it (audience: one) and it goes way beyond meeting that. That *(short*) stuff to compare/set one-character strings is pretty clever, even if it isn't portable. Personally I think little-endian is correct way to go, for many reasons. Seems PowerPC is one of the last holdouts (I know it has a little-endian mode, but I am pretty sure that causes extra overhead in some cases as compared to big-endian mode, like for unaligned accesses). For one, the 6502 would have taken an extra cycle all the time on absolute indexed instructions if it were big-endian, unless it threw extra hardware to compensate. The only downside is viewing in hex, but that is easily remedied by displaying from right-to-left. That way 78 56 34 12 gets displayed as 12 34 56 78.

by on (#63501)
blargg wrote:
The only downside is viewing in hex, but that is easily remedied by displaying from right-to-left.

But then that would take the "om" out of "homebrew".

But seriously, this sort of union-between-pointers-and-result-codes reminds me of techniques used in dynamically typed languages such as PHP and Python. If I were writing an assembler today, I'd probably do it in Python.

by on (#63511)
Dynamic typing takes all the fun out of it. It's more fun if you are able to pack a type field into a char*, without using any more bits than normal. Just hope your platform's malloc never returns (char*)1 as a valid memory block...

by on (#63518)
blargg wrote:
Just hope your platform's malloc never returns (char*)1 as a valid memory block...


You know the sad part is that, thanks to programming and/or emulation, I actually would worry about exactly that. "Hmm, there's a one in four billion chance that malloc could return an address of one. Meh, unacceptable. The risks are just too great."

You have to wonder about what kind of effects such a perfectionist mentality has on the rest of our lives :P

So, workaround time:

Code:
static const intptr_t False = NULL;
static const intptr_t True = &False;

by on (#63527)
intptr_t just complicates it. Use void* and it works much more smoothly (in C, due to implicit void* conversion), and fully portably:
Code:
static void* false_ptr = NULL;
static void* true_ptr = &true_ptr;
static void* po_ptr = &po_ptr;

const char* ptr;
ptr = false_ptr;
assert( !ptr );

ptr = true_ptr;
assert( ptr && ptr == true_ptr );

ptr = "str";
assert( ptr != false_ptr && ptr != true_ptr );

by on (#63528)
byuu wrote:
Hmm, there's a one in four billion chance that malloc could return an address of one.

Some platforms' ABIs specify that malloc() can't ever return a 1 in the three low order bits. For example, glibc guarantees 8-byte alignment. And with int being at least 2 bytes long on CHAR_BIT==8 machines like those that run the vast majority of emulators, I'd wager that every platform worth caring about aligns all pointers from malloc() to two bytes.

Yes, I just assumed that CHAR_BIT==8, but I can document that assumption in a compile-time assertion: code that results in declaring a negative-size array if it is false.
Code:
extern int CTASSERT_eight_bit_bytes[(CHAR_BIT == 8) ? 1 : -1];

by on (#63601)
tepples wrote:
Some


blargg wrote:
static void* true_ptr = &true_ptr;


Ah, I wasn't sure you could reference a variable you were defining for the first time like that. True = &False is such a lovely juxtaposition, though.

by on (#63615)
Indeed :)

Given that you can do the stupid int i=i there's no way you wouldn't be able to do void* p=&p.

by on (#68201)
I revisited asm6 and I believe I've got it working for big-endian. I've assembled a number of programs and they seem to work. I've made minimal modifictions, and included the original source I started with to make it easy to determine the changes.

asm6-big-endian.zip

When I was trying out asm6, I noticed several other more minor problems. I've currently fixed them in my own copy; is anyone else interested in these?

* Return an error status when it's not successful, so that a script won't keep running.
* Support quiet mode that doesn't print status messages, only errors, so that successful run prints nothing.
* Print errors to stderr.
* Check for write errors rather than ignoring (the final fwrite and fclose don't check).
* "wt" is not a valid file mode (there is no "t" modes; text is the absence of "b")

by on (#68210)
Cool. I'll look it over and add your changes to the main code.

by on (#68212)
How about the changes here?
http://nesdev.com/bbs/viewtopi ... 7686#67686

by on (#68219)
Maybe we should start a thread to bring together any modifications people have made, and cover any additional things to put into the main code (like the changes I mentioned I have in my private copy)?

by on (#68232)
Dwedit wrote:

Yes, that too. :P

by on (#68251)
Ah, it's great to see the official assembler being updated! =)

by on (#68542)
We don't have an "official" assembler. Yet.

by on (#68545)
I meant the official ASM6, the one maintained by loopy.