HNHacker News
TopNewBestAskShowJobs

rep_lodsb

1,024 karma · joined October 1, 2021

submissionscomments
rep_lodsb··on I designed a nibble-oriented CPU in Verilog to build a scientific calculator
Slight correction, the correct offset is 7, and DAA only adds 6. But the trick is also adding the carry bit. This works on the 6502 in decimal mode too, e.g. https://news.ycombinator.com/item?id=6342286

On the Z80 and 8086, the code can be made one byte shorter by taking advantage of adjust-after-subtraction, which the 8080 didn't have (and on 6502 worked differently):

    CP   10      / CMP  AL,10      ;set carry if valid decimal digit
    SBC  A,69H   / SBB  AL,69H     ;0..9 => 96h..9Fh (auxC=1), 10..15 => A1h..A6h (auxC=0)
    DAA          / DAS             ;subtract 66h if auxC set, 60h if clear
rep_lodsb··on A desktop made for one
I know this comment will get ignored by the true believers, and likely pasted directly into Claude by the author in order to "further improve" the code, but here's some small excerpts from the terminal emulator (glass.asm, 19360 lines, 555 KiB):

    cmp dword [rax], 'XAUT'
    jne .rxa_next
    cmp dword [rax+4], 'HORI'
    jne .rxa_next
    cmp word [rax+8], 'TY'
    jne .rxa_next
    cmp byte [rax+10], '='
    jne .rxa_next
    ; Found XAUTHORITY=path
Okay, this is setup code that only runs once at startup - but that would be a reason to optimize it for size and/or readability! REPE CMPSB exists, and may not be the fastest, but certainly the most compact and idiomatic way to compare strings. Or write a subroutine to do it!

This pattern is used everywhere for copying or comparing strings, this was just one example of it.

There's a state variable that's used to keep track of whether the input is text to be displayed or part of a control sequence. It's a full 64 bits, probably not because we need 18 quintillion states? Here's how it is evaluated:

    ; Dispatch based on state
    mov rcx, [vt_state]
    cmp rcx, VT_ESC
    je .vtp_esc
    cmp rcx, VT_CSI
    je .vtp_csi
    cmp rcx, VT_CSI_PARAM
    ...
In total, there are 7 compares + conditional jumps, one after another. Compilers would generate a jump table for this, and a better option in assembly might be to make vt_state a pointer to the label we want to go to. Branch predictors nowadays can handle indirect jumps, and may actually have more trouble with such tightly clustered conditionals as seen in this code.

This code is on the "slow" path, there's a faster one for 7-bit ASCII outside of control sequences, with a lengthy comment by Claude at the top on how it optimized this. Even this one starts with a bunch of conditionals though:

    cmp qword [vt_state], 0            ; VT_NORMAL == 0
    jne .vtp_loop_slow
    cmp dword [utf8_remaining], 0
    jne .vtp_loop_slow
    cmp byte [pending_wrap], 0
    jne .vtp_loop_slow
These could likely all be condensed into a single test or indirect jump via the state variable, by introducing just a few more states for UTF-8 decoding and wrap. Following this, here's a "useless use of TEST" (the subtraction already set the flags):

    mov rbx, [grid_cols]
    sub rbx, [cursor_col]              ; rbx = cells left on this row
    test rbx, rbx
    jle .vtp_loop_slow                 ; no room (or already past)
This also again shows the compulsive use of 64-bit registers and variables for values that should never be this big. It's not the "natural" data size on x86-64 at all, every such instruction requires an extra prefix byte.

I freely confess that I'm a "Luddite", and was explicitly looking for bad (and obviously so) code, but this took me just a few minutes of scrolling through the nearly 20K lines in this file, so it should be somewhat representative of the whole.

rep_lodsb··on Why are there both TMP and TEMP environment variables? (2015)
Fun fact: "/dev/nul" (with only one L) would have worked, even if there is no directory with that name.

That's been a feature since DOS 2.0, there was even an undocumented option AVAILDEV to make the prefix mandatory, instead of having device names present everywhere. But it broke the common trick used to detect if a directory exists ("if exist c:\some\path\nul").

rep_lodsb··on Microsoft open sources DOS 1.00 on 45th anniversary
Last I checked, it's still mostly unusable, especially on real hardware (both modern and Win2k-era). Not saying this because I have anything against it, that's just a fact.

Of course one could say that Windows 11 provides negative value, in which case running DOS 1.00 would also be better ;)

rep_lodsb··on GitHub – DOS 1.0: Transcription of Tim Paterson's DOS Printouts
The original code using ROL should have been correct? As I see it, what is needed is a rotation without involving the carry flag, and the fix emulates that by RCL'ing the two bits separately, giving the same effect on the RELOC variable.

This may actually have been a bug not in the code, but in the assembler used to build it. The 8080 had mnemonics ROL and ROR that rotated through carry, and RLC/RRC (standing for "rotate circular", not "through carry"). Opposite meaning of the 8086 mnemonics! So I suspect this may have been switched up in the assembler, especially if it was running on an 8080 machine and developed by someone more familiar with its instruction set.

The STORE bug would have prevented using files over the size of 512 bytes, not just 64K. It's dividing by the sector size, and if DX was greater than that, it would have caused a "divide overflow" exception, since the result wouldn't fit in 16 bits.

(Also, by the Laws of Robotics you have to tell me if you're an LLM, or used one to generate this comment.)

EDIT: not an assembler bug, it seems. The printed listing shows that it produced the correct opcode for ROL (RCL would be D1 D1):

    0A28 8A 0E D4 1B           1353  MOV CL,[RELOC]
    0A2C D1 C1                 1354  ROL CX
    0A2E D1 C1                 1355  ROL CX
    0A30 88 0E D4 1B           1356  MOV [RELOC],CL
So I don't know why this version of the code wouldn't have worked. Maybe the penciled-in "fix" was to free up CL for some other purpose?
rep_lodsb··on Math Is Hard – OpenBSD Stories
Division by zero is handled the same on x86, and also changed from "trap" to "fault" after the original 8086/8088. And despite what is often said about its variable-length instruction format, skipping over opcodes is pretty much trivial compared to VAX.

Early versions of Microsoft Flight Simulator included a handler for the divide exception, which adjusted the result to +/- "infinity" (for a 16-bit signed integer, 32767 or -32768). The rest of the code relied on this in order to work correctly, and it was more efficient to take advantage of the processor's microcode doing this check rather than coding it explicitly before every division.

So even if it doesn't make mathematical sense, being able to continue after this type of exception is a useful feature to have.

>(and if you ignore SIGSEGV, it is considered perfectly acceptable that your program spins in a SIGSEGV loop until you kill it.)

This, on the other hand, shouldn't ever be acceptable. If a fatal-by-default signal is just ignored, it should always terminate the process.

rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
Most of mul/div was implemented in hardware since the 80186 (and the more or less compatible NEC V30 too). The microcode only loaded the operands into internal ALU registers, and did some final adjustment at the end. But it was still done as a sequence of single bit shifts with add/sub, taking one clock cycle per bit.
rep_lodsb··on Windows 9x Subsystem for Linux
That's greatly oversimplified, or less generously, just flat out wrong. Win32 programs have always had their own isolated address space. That infamous BSOD is the result of memory protection hardware catching an access to something outside of that address space. When you open a DOS box, it uses the paging and V86 hardware mechanisms to create a new virtual machine, even though it shares some memory with the instance of DOS from which Windows was booted.

What Windows 9x didn't have was security. A program could interfere with these mechanisms, but usually only if it was designed to do that, not as a result of a random bug (if the entire machine crashed, it was usually because of a buggy driver).

rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
I don't doubt that this specific processor special-cased XOR (regardless of how it was called in the assembly language)!

Merely pointing out that where both operations were available, there seems to have been a preference to use SUB instead, with some continuity from early business-oriented mainframes, to the 360, to the PC.

rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
These two steps usually run in parallel though, with transistors to enable them depending on what operation should be performed.
rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
Some other architectures like PDP-11 and 680x0 had a dedicated "clear register" instruction.

It could have been added to x86, even as a group of single-byte opcodes with the register encoded in three bits (as with PUSH, POP, and INC/DEC outside of long mode). But the XOR idiom was already established on the 8080 by that point.

rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
Interesting, since the general culture at IBM seems to have preferred SUB over XOR -- their earlier business-oriented machines didn't even have a XOR instruction, and even on later ones the use of SUB has persisted, including in the IBM PC and AT BIOS.

(There was another, now deleted, comment somewhere in this thread that mentioned IBM's preference for SUB. Source of that statement was Claude, but it seems very likely to be correct. The BIOS code I've checked myself, lots of 'SUB AX,AX', no XOR)

rep_lodsb··on XOR'ing a register with itself is the idiom for zeroing it out. Why not sub?
Comparing for equality can use either SUB or XOR: it sets the zero flag if (and only if) the two values are equal. That's why JE/JNE (jump if equal/not equal) is an alias for JZ/JNZ (jump if zero/not zero).

There's also the TEST instruction, which does a logical AND but without storing the result (like CMP does for SUB). This can be used to test specific bits.

Testing a single register for zero can be done in several ways, in addition to CMP with 0:

    TEST AX,AX
    AND  AX,AX
    OR   AX,AX
    INC  AX    followed by DEC AX (or the other way around)
The 8080/Z80 didn't have TEST, but the other three were all in common use. Particularly INC/DEC, since it worked with all registers instead of just the accumulator.

Also any arithmetic operation sets those flags, so you may not even need an explicit test. MOV doesn't set flags however, at least on x86 -- it does on some other architectures.

rep_lodsb··on 80386 Memory Pipeline
Yes, the far jump was never necessary on any processor, only a convention. You can stay in the same segment as in real mode and it will continue to work. But some kind of control transfer to flush the queue must be done shortly after the LMSW / MOV CR0, or things may break in ways that I'm not entirely clear on.

My test code looked like this:

        mov     ax,1            ;new MSW
        mov     bx,TestSel      ;pointer to selector value into BX
        mov     dx,[bx]         ;and load into DX
        mov     cl,31           ;shift count for delay
        cli                     ;disable interrupts
        lgdt    [Gdtr]
        lidt    [Idtr]
        jmp     enter_pm        ;flush queue now
               
    align 2
    enter_pm:                   ;go!
        rol     cl,cl           ;delay while following instructions decode
        lmsw    ax              ;set PE bit
        mov     es,[bx]         ;should load selector 0x0010 into ES
        mov     ds,dx           ;should set DS base to 0x00100 [NOPE]
        str     ax              ;should trap because not allowed in real mode
        ud2                     ;trap anyway in case it didn't
On the 286, this always caused the processor to reset. Replacing one of the two segment load instructions with a same-length "mov ax,ax" didn't change that, but removing one of them did.

In that case the "str ax" acted as the control transfer that flushed the queue (it was still decoded in real mode, so it went to the "invalid opcode" entry point). No clue as to what exactly happens to cause the reset when three instructions are run from the queue, some timing issue related to when the PE bit actually changes vs. what the decoder is doing at this point?

rep_lodsb··on 80386 Memory Pipeline
Well, Intel documented that the very first instruction after enabling protected mode had to be an "intra-segment" (not inter-segment) jump, to flush the prefetch queue. At least that was what it said in the 286 and 386 documents I read. You were supposed to set up everything else needed before that, do this near jump, and then jump to the new protected mode code segment.

Some later documentation contradicted this, saying that instead this first jump had to be to the protected mode segment.

From the patent (US4442484), it is apparent that the processor decodes opcodes into a microcode entry point before they are executed, and the PE bit is one of the inputs for the entry point PLA. So that would be the obvious reason for flushing the prefetch queue - but it turns out that at least on the 80286, most instructions go to the same entry point regardless of the mode they are decoded in. So they should work the same without flushing the queue.

And yet for some reason, what I've seen in my experiments is that the system would reset if there were three instructions following the "LMSW" without a jump. Even something harmless like "NOP" or "MOV AX,AX", that couldn't be different between real and protected mode. Maybe there is some clock phase where the PE bit changing during the decoding of an instruction leads to an invalid entry point, that either causes a triple fault or resets the processor?

rep_lodsb··on 80386 Memory Pipeline
OP probably meant to post in another thread: https://news.ycombinator.com/item?id=47767398
rep_lodsb··on 80386 Memory Pipeline
Again a very interesting look at how this chip works internally!

I've decoded the entry point PLA of the 80286 (not the actual microcode though). It also has separate entries for real and protected mode, but only for segment loads from a general purpose register, HLT, and for those opcodes that aren't allowed in real mode like ARPL.

Loading a segment register from memory on the 286 uses the same microcode in both modes, as does everything else that would certainly have to act differently, like jump/call far. That was a bit surprising, since it would have to decide at run time which mode it's in. Is this the same on the 386?

Tested on my 286 machine what happens when opcodes are decoded while in real mode but executed after PE is set: Segment load from memory works (using protected mode semantics), whereas the load from register only changes the visible selector and nothing else. The base in the descriptor cache keeps whatever was set there before -- I assume on the 386, SBRM would update the base the same way it does in real mode in that situation, because it's also used for V86 mode there. Illegal-in-real-mode instructions trap, but do so correctly using the protected mode IDT.

Also seems like executing three pre-decoded instructions without a jump after setting PE causes a triple fault for some reason.

rep_lodsb··on "cat readme.txt" is not safe if you use iTerm2
https://en.wikipedia.org/wiki/+++ATH0#Hayes'_solution

What was patented was the technique of checking for a delay of about a second to separate the command from any data. It still had to be sent from the local side of the connection, so the exploit needed some way to get it echoed back (like ICMP).

More relevant to this bug: https://en.wikipedia.org/wiki/ANSI_bomb#Keyboard_remapping

DOS had a driver ANSI.SYS for interpreting terminal escape sequences, and it included a non-standard one for redefining keys. So if that driver was installed, 'type'ing a text file could potentially remap any key to something like "format C: <Return> Y <Return>".

rep_lodsb··on Rewriting Every Syscall in a Linux Binary at Load Time
Thinking a bit more about it (and reading TFA more carefully), what's the point of rewriting the instructions anyway?

I first assumed it was redirecting them to a library in user mode somehow, but actually the syscall is replaced with "int3", which also goes to the kernel. The whole reason why the "syscall" instruction was introduced in the first place was that it's faster than the old software interrupt mechanism which has to load segment descriptors.

So why not simply use KVM to intercept syscall (as well as int 80h), and then emulate its effect directly, instead of replacing the opcode with something else? Should be both faster and also less obviously detectable.

rep_lodsb··on "cat readme.txt" is not safe if you use iTerm2
For this use case, there would also have to be an extension to the SSH protocol to send such out-of-band information. Maybe this already exists and isn't used?

The broader problem with terminal control sequences didn't exist on Windows (until very recently at least), or before that DOS and OS/2. You had API calls to position the cursor, set color/background, etc. Or just write directly to a buffer of 80x25 characters+attribute bytes.

But Unix is what "serious" machines -a long time ago- used, so it has become the religion to insist that The Unix Way(TM) is superior in all things...

rep_lodsb··on "cat readme.txt" is not safe if you use iTerm2
From your previous post:

>It's user data in JSON in an HTTP stream in a TLS record in a TCP stream in an IP packet in an ethernet frame. Then it goes into a SQL query which goes into a B-tree node which goes into a filesystem extent which goes into a RAID stripe which goes into a logical block mapped to a physical block etc. All of those have control data in the same stream under the hood.

It's true that a lot of code out there has bugs with escape sequences or field lengths, and some protocols may be designed so badly that it may be impossible to avoid such bugs. But what you are suggesting is greatly exaggerated, especially when we get to the lower layers. There is almost certainly no way that writing a "magic" byte sequence to a file will cause the storage device to misinterpret it as control data and change the mapping of logical to physical blocks. They've figured out how to separate this information reliably back when we were using floppy disks.

That the bits which control the block mapping are stored on the same device as a record in an SQL database doesn't mean that both are "the same stream".

rep_lodsb··on "cat readme.txt" is not safe if you use iTerm2
>All of those have control data in the same stream under the hood.

Not true. For most binary protocols, you have something like <Header> <Length of payload> <Payload>. On magnetic media, sector headers used a special pattern that couldn't be produced by regular data [1] -- and I'm sure SSDs don't interpret file contents as control information either!

There may be some broken protocols, but in most cases this kind of problem only happens when all the data is a stream of text that is simply concatenated together.

[1] e.g. https://en.wikipedia.org/wiki/Modified_frequency_modulation#...

rep_lodsb··on Rewriting Every Syscall in a Linux Binary at Load Time
Yes, that seems unneccessary. The overhead of trapping and rewriting every syscall instruction once can't be (much) greater than that required for rewriting them at the start either.

Even if you disallow executing anything outside of the .text section, you still need the syscall trap to protect against adversarial code which hides the instruction inside an immediate value:

    foo: mov eax, 0xc3050f    ;return a perfectly harmless constant
         ret
    ...
    call foo+1
(this could be detected if the tracing went by control flow instead of linearly from the top, but what if it's called through a function pointer?)
rep_lodsb··on Show HN: BAREmail – minimalist Gmail client for bad WiFi
Dedicated mail clients have existed for a lot longer than GMail has, work with any service using the POP3 or IMAP protocol, and don't run inside a web browser.
rep_lodsb··on German implementation of eIDAS will require an Apple/Google account to function
It's doing things that are against the interest of the user. But obviously, that's no longer an acceptable definition! According to our benevolent overlords, Android is definitely not malware, while yt-dlp is </s>
rep_lodsb··on German implementation of eIDAS will require an Apple/Google account to function
There's also a problem with unmodified phones containing malware, namely an operating system made by an advertising company, which is designed to collect as much information about you as possible.

And this malware is largely based on open source code (Linux) that was originally developed on open, documented hardware, where the firmware boot loader did nothing more than load the first 512 bytes of your hard disk to address 0x7c00 and transfer complete control to it.

Yes, there were viruses that exploited this openness, but imagine if Linus Torvalds would have needed a cryptographic certificate from IBM or Microsoft to be allowed to run his own code! This is basically the situation we have today, and if you don't see how dystopian this is, I don't know what more to say.

I will never understand why such an overwhelming majority of people seem to just accept this. When frigging barcodes where introduced, there were widespread conspiracy theories about it being the Mark of the Beast -- ridiculous of course, but look at now where in some places you literally can't buy or sell without carrying around a device that is hostile to your interests. And soon it will be mandated by the state for everyone.

Google must be destroyed.

rep_lodsb··on Turbo Pascal Deconstructed
For the code generator, it produced this annotated disassembly:

    2100 push ax            ;--- EmitByte: write one byte to code output ---
    2101 mov di, [code_ptr] ;DI → current position in output buffer
    2104 stosb              ;Write AL to output, advance DI
    2105 mov [code_ptr], di ;Update code pointer
    2108 pop ax             ;Restore AX
    2109 ret                ;Every compiled instruction flows through this 6-instruction emitter
    2110 mov al, 0E8h       ;--- EmitCall: generate CALL instruction ---
    2112 call EmitByte      ;Emit opcode byte E8h (near CALL)
    2115 sub bx, [code_ptr] ;Calculate relative offset
    2118 sub bx, 2          ;Adjust for instruction length
    211A xchg ax, bx        ;AX = relative offset
    211B call EmitWord      ;Emit 16-bit relative displacement
    211E ret                ;Generated: E8 lo hi — a complete CALL instruction
Obviously, there has to be a lot more to even a simple-minded x86 code generator than just a generic "emit opcode byte" and "emit call" routine. In general, what A"I" produced here is not a full disassembly but a collection of short snippets, potentially not even including the really interesting ones. But is it even correct?

EmitByte here is unnecessarily pushing/popping AX, which isn't modified by the few instructions in between at all. No competent assembly language programmer would do this. So maybe against all expectations, Turbo Pascal is just really badly coded? No, it's of course a hallucination: those instructions don't appear in the binary at all!

That the hex addresses are wrong can already be seen in the instruction "mov di,[code_ptr]" here being apparently only three bytes long. In reality it would take four! And it's easy to confirm that this code isn't present at the addresses shown.

So maybe it's somewhere else? x86 disassembly can be complicated because the opcodes are variable length, and particularly in old programs like this the code and data are often not cleanly separated. Claude apparently ran it through NDISASM, which doesn't even attempt to handle that task.

But searching for e.g. the hex opcode B0 E8 ('mov al,0xe8') is enough to confirm that this code snippet isn't to be found anywhere.

There is a lot more suspicious code, including some that couldn't possibly work (like the "ret 1" in the system call dispatcher, which would misalign the stack).

Conclusion: it's slop

rep_lodsb··on Never Bet Against x86
Yes, you can detect signed overflow that way, but it's a lot more instructions so it won't be used in practice.

The designers of RISC-V included the bare minimum needed to compile C, everything else was deemed irrelevant.

rep_lodsb··on My application programmer instincts failed when debugging assembler
>Protip: your functions should be padded with instructions that'll trap if you miss a return.

Galaxy brained protip: instead of a trap, use return instructions as padding, that way it will just work correctly!

Some compilers insert trap instructions when aligning the start of functions, mainly because the empty space has to be filled with something, and it's better to use a trapping instruction if for some reason this unreachable code is ever jumped to. But if you have to do it manually, it doesn't really help, since it's easier to forget than the return.

rep_lodsb··on Never Bet Against x86
That only works for unsigned integers.
← PreviousPage 2 of 19Next →