6

I have spent a few years coding in C and Java. I enjoy how I can make a program display a message box or perform some file operations and it works on every (I think?) CPU without any dramas. Although I have never coded anything advanced.

I want to learn assembly, for Windows only. I believe I should learn something called x86 asm.

My question: provided I do not want to do something too crazy or obscure, will programs I create work on every CPU? My focus is on your average computer at home or perhaps servers.

I have had many people tell me that I have to pick a specific architecture for a specific CPU from many reputable sources, but I am then shown code examples that work on several different CPUs... conflicting information!

EDIT: I just want to write some programs for fun in assembly without having to worry if my friend John Doe will have trouble using it on his computer. I can write a program in C easily enough that will display a message, ping google.com and whatever else without having to worry about CPUs. Is asm really not the same? o.O

EDIT2: Maybe I am getting confused as I do not know which CPUs a normal (nothing fancy) program coded on Windows in C works on... just Intel and AMD?

securityauditor
  • 293
  • 3
  • 13
  • 1
    The IBM PC historically evolved around Intel processors, starting with 8086, then going through 80186, 80286 (common 16 bit), 80386 (32 bit extended), 80486, Pentium (sometimes referred to as 80586), then later the naming/numbering is not anymore that simple, from this you can probably see yourself what "x86" means. Generally the newer models are fully backward compatible, i.e. even modern Intel i7 CPU is capable to operate in 16 bit real mode supporting all 80286 instructions, so you can boot and run old DOS system from 1990 (the HW driver problems will make it non-practical or fail, not CPU) – Ped7g Jun 18 '18 at 11:42
  • 4
    x86 assembly will work on x86 processors. It won't work on ARM processors, such as commonly found in mobile phones. So yes, you have to pick a specific architecture but usually not a specific cpu model (but you might need to work with different instruction set extensions) – Jester Jun 18 '18 at 11:43
  • Today the question about x86 assembly is 32 or 64 bit, while 32 bit may be a tiny bit easier to learn (more books, been around for 25+ years), it's becoming slowly obsolete. While the 64 bit mode is sufficiently different to need quite some study to adjust, the 32 bit knowledge will help. If you are just starting, the user-land 32 bit protected mode under win32 linking against C standard library is probably easiest one to learn, as you can focus mostly on pure algorithmic tasks and let I/O and HW specifics to OS+C_lib. I would start with 386 (or 586) instruction set, has all basic instructions – Ped7g Jun 18 '18 at 11:47
  • 1
    Then x86_64 is "current", you can re-learn the basics in reasonable time, and such machine code will still work on every x86_64 CPU (most of the windows PCs today) (but only under your particular OS). Then modern CPUs have many extensions, like SSE2 is baseline for 64b CPUs, but then there are many optionals (AVX with different generations, SSE3/4, etc), which are essential if you are trying to optimize some real world code for performance, but not needed at all if you are just starting with assembly and learning basic principles. There's no universal assembly, it's always per CPU+OS. – Ped7g Jun 18 '18 at 11:52
  • hmm... didn't find duplicate within 60s, and the question seems answerable ... I will probably convert my comments + extend into full answer, but waiting if somebody will find some nice duplicate.. Anyway, the short answer is: machine code using only 80686 instruction and interacting with Win32 API, will be executable on most of the modern windows computers, as that requires like less than 20 year old Intel or AMD CPU. But it will not work on ARM or other type of CPU, and on other OS even on x86 CPU. If you will use f.e. SSE4-extension instructions, it will run only on some [modern] x86 PCs. – Ped7g Jun 18 '18 at 11:59
  • 4
    See links in https://stackoverflow.com/tags/x86/info for some beginner guides, reference material, and advanced stuff. And performance-tuning stuff. – Peter Cordes Jun 18 '18 at 12:00
  • Probably the major pain is the quality of your tutorial/book and tools they prefer, with bad tutorials you will suffer a lot, no matter what kind of tools they will pick. You can usually decide on the quality of the tutorial/book by it's length (short = bad), then reading few chapters (not understandable or too easy to understand = bad, probably lacking on details), expect the asm to be somewhat tricky (yet very simple in principle, but many details everywhere and following "common sense" of HW design, not the high level programming languages like C/Java, so it may look weird sometimes. – Ped7g Jun 18 '18 at 12:07
  • About John Doe friend and *"I can write a program in C easily"* - exactly, you can **write** it easily (thanks to standard C library), but not distribute the binary. The ".exe" file produced on your system will work only on compatible computers (with default settings your compiler will probably use low-enough set from the x86 ISA), so if you will build it as 32 bit app, it will work on most of the x86 computers out there, except some prehistorical pieces ... it will not work on ARM PCs, unless the visual studio has some multi-platform fat binary target, including machine code for both x86+ARM. – Ped7g Jun 18 '18 at 12:53
  • @Ped7g Pretend I make a program (nothing fancy) on a Windows computer coded in C. 1) What CPUs will this C program work on by *default*? 2) What asm do I use to make the same program that works on the same CPUs as that C program (because it seems like I would need to take 9999 different versions of that program for each CPU)? – securityauditor Jun 18 '18 at 13:22

5 Answers5

8

The IBM PC historically evolved around Intel processors, starting with 8086, then going through 80186, 80286 (still 16 bit only), 80386 (32 bit extended), 80486, Pentium (sometimes referred to as 80586), Pentium II (686 sometimes used for this family of CPUs), then later the naming/numbering is not that simple any more.

From these the "x86" platform name was created, as all of the early models did end with "86" and the digit ahead of 86 was modified.

The "x86" family of CPUs is quite specific in the way how the new models were designed - to be [almost] fully backward compatible. So the 80186 and 80286 CPUs can run the 8086 machine code as is, they used all the original 8086 instructions, and just introduced new extension on top of that.

The first considerably different CPU was 80386, which introduced new 32 bit mode with 32 bit registers and protected mode (The 286 "protected mode" got mostly ignored and was only 16 bit). The backward compatibility is still achieved, as the 80386 after power-on will start in 16 bit mode, working as faster 80286 with few more extra instructions, then the code can switch it to 32 bit mode as needed (usually the modern OS bootloader will, very early in the OS loading process).

Then 80486 and next Intel CPUs were again backward compatible with 80386, just extending the original instruction set (and since 80486DX CPUs and Pentiums, every x86 CPU has now the floating point unit built-in by default, although with modern x86 it's obsolete ... for older 80386 and 80486SX CPUs one had to buy separate x87 coprocessor chip for HW FPU).

Meanwhile Intel tried to release itself from the backward-compatibility prison (which makes design of modern x86 CPU very complex and cumbersome), by introducing brand new 64 bit platform ("Itanium" or "IA-64"), which was NOT backward compatible. The market did hesitate with adoption a lot, unsurprisingly, because all the old "x86" SW didn't work on it.

A bit later AMD introduced it's own 64 bit extension, this time designed around "x86" legacy in similar way how the 386 extended 286, which did work much better from customers point of view (although it means for assembly programmer it's a tiny bit more tricky, like for example mov eax,1 will modify whole 64 bits of rax register, zeroing the top 32 bits automatically, etc... some of these quirky "rules" made the 64 bit extension of original 32 bit instruction set feasible, and in the long run it works quite nicely, even if it may feel a bit hack-ish at first read).

So modern "x86" PC does contain like three major different CPUs in single chip, the obsolete 16 bit 80286, the very old 32 bit "686", and current 64 bit "x86-64" variant. On assembly point of view all of them share the basic instructions and syntax, so if you learn "686" instruction set fully, the x86-64 source code will looks mostly familiar to you. And as long as you will use only the basic subset of instruction set (like the "686" subset), your binary will work on the particular OS on all 15-20y old x86 PCs.

Meanwhile the AMD (and Cyrix and other manufacturers who tried to compete with Intel in the "x86" world) are producing CPUs which are binary compatible with Intels. Sometimes they tried to introduce some extensions, like "3Dnow" from AMD, but they were used by programmers only rarely, and usually got abandoned. The only exception is the current 64 bit mode, which was designed by AMD and Intel in the end had to give up and copy it from AMD into their CPUs (as their Itanium IA-64 was not accepted by market, due to missing backward compatibility).

But if you are like game developer, going after maximum performance, you will have to check the CPU model/features in runtime and depending on the availability of the instruction extensions, you can provide different variants of binary for particular CPUs, like one done with SSE2 instructions at most, being capable to run on all 64 bit CPUs, and other variants using also new extensions like SSE4 and AVX512, which will work only for the minor group of consumers, who have the latest CPUs.

Many C compilers by default on x86 target 32 or 64 bit mode (depending mostly on OS, or your project settings) and use only very limited instruction set, like mostly "686" only, or x86_64 without even SSE1/2, so their binaries will work on any common x86 PC. Although the code is sub-optimal, not using the modern features of current CPUs.

That's also the first thing you can learn, if you are targetting "x86" assembly knowledge, start with the basic instruction set, like maybe only 80386, learn principles on that, then see how it was extended later (skip 16 bit 80286, that's just useless torture, the 32 bit mode is lot more simpler to learn, then if you are really curious, you can try to see how the 16 bit differs, the 32 bit experience will make it somewhat easier, but it's quite pointless).

The 64 bit is tiny bit more tricky/complex than 32 bit mode, but it's not that bad, you can start even with that, if you wish (nowhere as tricky, as 16 bit was).

BTW, you should still start with C-lib for I/O and win API calls, i.e. you will need C compiler. Accessing Win API from pure ASM is somewhat tricky (just a needless distraction, if you are just learning ASM basics), and you can't access HW directly under modern OS, so there's no way to do I/O without OS API services, unlike the old 16 bit mode, where there was no protection and you could access HW peripherals freely. You can call your asm functions from C wrapper, which may then handle the I/O, memory management, and other OS API related things, focusing on pure algorithms and asm programming in your asm functions (that's also how assembly is used in real projects, you don't write whole app in asm today, you keep it in C/C++ and rewrite only the most performance critical parts in ASM, after you have working C/C++ prototype and you did identify particular bottleneck by profiling .. there's no point to write other parts in assembly, like file reading, etc, too cumbersome without any benefit).

BTW, The C is portable language. If you will use only standard C library, it will work (the SOURCE) on pretty much anything, but only after you will COMPILE it for particular target platform BINARY.

The windows is very small world, mostly limited to x86 (there were windows variants for IA-64 and DEC Alpha, which can't run x86 binaries, but those were professional machines at specialized work, not known much to public). So if you compile Win32 x86 executable with default options, in will run (sub-optimally) on 99% of windows computers, using only limited sub-set of x86 instructions. If you will switch on the compiler to use some modern features of your CPU, the resulting binary may not work any more on John Doe's PC, if he has x86 CPU which doesn't support one of the features your machine code does use.

For many applications this default sub-set is more than enough, and you don't need to bother with the extended instructions. Only few applications need maximum performance, like games, CFD or other scientific calculations... your ordinary web browser operating with +5-10% performance penalty can be safely ignored (and there's usually somewhere in driver/etc some fine-tuned piece of code for your current CPU, which does handle the major performance hungry things, like decoding video/etc, so even web browser compiled with generic "x86" target will benefit from those).

You don't need to produce 9999 variants of asm code, if you want just to make it run, basically you need only 32 or 64 bit variant (depending on the target, I don't know about windows world, but with modern OS you can safely target 64 bit only, that's like 90+% of users), using the basic x86 instructions, that will work "everywhere" (on x86 windows).

But there's not much point to use asm like that (except educational purposes, where it makes perfect sense to start exactly with this), because the performance of such asm will be sub-optimal, so you can already use C or C++ to get similar results. For meaningful usage you have to learn also the modern extensions, do the runtime check for available features, and dynamically load the correct variant of your function, using the optimal machine code for particular CPU type. That's the part where you may have to write 9999 variants of same function (it's not that bad, usually 4-7 variants will probably cover most of the available CPUs, one compatibility fallback using only basic instruction set, then few more specialized, like SSE3, +SSE4, AVX1/2, AVX512, etc..). Also the real world SW needing maximum performance is very rare, even most of the simpler games are completely fine with sub-optimal binaries, only if you are working on some bleeding edge like Unreal-engine developer, you have to care about those fine tunings. Most of the time the gains are not worth the investment.

After all, there's now so much SW in use which is written in C#, Java, or even JavaScript (or PHP .. did I really mention it? Feels dirty now), that obviously the performance is not a problem, otherwise most of that would be rewritten into C++ after some time, to get the performance boost.

Ped7g
  • 16,236
  • 3
  • 26
  • 63
5

You keep claiming that a program written in C can be used by someone else on their computer without problems. That's only true if they're also running Windows, not Linux or OS X, or OpenBSD, or Solaris on a SPARC workstation...

The C libraries you'd have to use to display windows or do network I/O ("ping google.com") on other OSes are different. Especially ping is a very non-portable thing, because sending ICMP echo-request packets is usually a privileged operation. (e.g. the ping executable on Linux is setuid-root, so it can enforce rate-limits.) You'd probably do it with system("ping google.com") rather than using network sockets within your own program. A better example would be making an HTTP request, because any process can open a TCP connection.

You can write portable C programs, but you definitely have to work at making them portable. And people need the source so they can compile it for their own system. (This is why Unix has a tradition of distributing software in source form: everyone needed to compile it for their own system with their own versions of libraries.)

Compiled binaries will only work on the target platform they're compiled for, e.g. x86-64 Windows. Such a binary wouldn't work on ARM Windows, or 32-bit-only x86 Windows.


When writing in asm, your source is specific to a target platform (including the CPU, not just which system calls and libraries are available). So you'd be writing code for 32-bit x86 Windows, instead of just "for Windows" with the ability for a C compiler to make an x86 32-bit or an x86-64 64-bit binary from the same source.

Not a big difference if all you cared about was 32-bit x86 Windows in the first place. (Every "normal" Windows computer can run 32-bit x86 binaries, so that's what you should make if you want to be portable to other Windows computers.)

For example, lets use the Godbolt compiler explorer (source+asm) to see how a trivial C program compiles to different asm for two different x86 platforms:

#include <stdio.h>
int main() { puts("Hello World"); }

compiles to this asm for x86-64 Linux (x86-64 System V calling convention), with gcc -O3 (Intel-syntax mode instead of AT&T, Godbolt passes it -masm=intel):

.LC0:
    .string "Hello World"
main:
    sub     rsp, 8                   # align the stack before a call
    mov     edi, OFFSET FLAT:.LC0
    call    puts                     # first arg passed in RDI
    xor     eax, eax                 # eax = 0 = return value.
    add     rsp, 8                   # restore the stack
    ret

But MSVC for 32-bit x86 Windows compiles it to this asm:

$SG5328 DB        'Hello World', 00H
EXTRN   _puts:PROC
_main   PROC
    push     OFFSET $SG5328
    call     _puts                    ; first arg passed on the stack
    add      esp, 4                   ; clean up args
    xor      eax, eax                 ; eax = 0 = return value.
    ret      0
_main   ENDP

The assembler syntax is different (MASM vs. GAS), but that's not the important thing. The important thing is that the pointer to the string literal is passed on the stack (with push) instead of in a register.

The asm symbol name for puts is prefixed with an _ on Windows, but not on Linux.

And since this is 32-bit code, the width of a stack slot is only 4 bytes, not 8.

This is for ISO C functions like puts that are available on both platforms. On Windows you could call MessageBoxA in a WinAPI DLL, but Linux doesn't have a "native" graphics API. You'd need an X11 library for most desktops, but not everyone uses X11.


Since you already know C, looking at compiler output is a good way to get started learning asm. See Matt Godbolt's CppCon2017 talk “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”. (Also further advice on writing tiny functions that compile to interesting asm: How to remove "noise" from GCC/clang assembly output?).

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • You made the linux example to use Intel syntax any way, not GAS. – Ped7g Jun 18 '18 at 14:18
  • 1
    @Ped7g: I didn't even think about it. That *is* GAS syntax, though: note the `.string`. It's GAS `.intel_syntax noprefix`. I normally use Godbolt in Intel-syntax mode so it's less mental context-switching between NASM source, Intel manuals, and compiler output. I usually only use AT&T syntax for GCC bug reports, or when answering SO questions that use AT&T syntax. Anyway, it's much easier to spot the differences between two MASM-like blocks that both use `OFFSET symbol_name` than if it had been `mov $.LC0, %edi`, so I think it's better this way. – Peter Cordes Jun 18 '18 at 14:20
  • yup.. I agree on keeping it as is, and you are right the `.string` part is GAS syntax, but the Intel flavour... :D ... it's such a joy to talk about x86 asm syntax... Only about 100 different variants of them... :D (before even touching the platform differences) – Ped7g Jun 18 '18 at 14:28
3

Let us take it step by step:

  • The asm code is the same if the processor is the same, the OS doesn't matter, so you don't have to focus on Windows.
  • Yes, you have to choose a specific architecture (or you should). Each architecture has different instructions set. Further instructions, you have to understand the architecture (registers, address, busses...) in order to design a proper program, so definitely it is non-trivial.
  • asm is not easy, actually is very hard, so if the goal is complex tasks (servers?), hard turns into hardly impossible. If this is a problem (or the architecture), you may try a higher level, such as C.
  • Might work a binary in different architectures? Yes, but it is just coincidence (or compatible, as Intel 8080 on Intel 8085).
Jose
  • 3,306
  • 1
  • 17
  • 22
  • 2
    8080 is *not* binary compatible with x86. If it was *binary* compatible, it would *be* an x86. [The start of x86: Intel 8080 vs Intel 8086?](https://retrocomputing.stackexchange.com/a/6501). – Peter Cordes Jun 18 '18 at 12:01
  • 2
    asm is only hard when you don't know / understand it. If you know C and asm equally well, implementing *small* functions in asm is not particularly harder than C, just more time consuming. (Especially if you want it to run fast.) Of course you don't want to develop large programs in asm, because compilers are better at optimizing at that scale than humans can be while still writing maintainable code. – Peter Cordes Jun 18 '18 at 12:03
  • @PeterCordes yes, absolutely right. I have edited 8000 to 8085. Thank you for the note. – Jose Jun 18 '18 at 12:04
  • 2
    For a total beginner, yes you do need to make sure the tutorials you're following are for Windows. System calls are OS-specific, so you can't just find some code with DOS `int 21h`, or Linux or OS X `int 0x80` calls for input/output and expect it to work. It will assemble (if the syntax is the same), but crash at runtime. The calling conventions even differ between Windows x64 and x86-64 System V (shadow space, arg-passing registers, and which are call-preserved). – Peter Cordes Jun 18 '18 at 12:06
2

Assembler code don't work on CPUs. It needs to be transformed to machine code by the program called assembler. That transformation is usually simple (usually, the assembler program transforms text containing assembler code into some object code; the linker then agglomerates several object files into an executable - and resolve relocations; that executable contains machine code).

There are several assembler syntaxes for x86 (e.g. nasm vs gas).

And there are several variants of x86 (e.g. some processors, but not all, accept extensions like AVX, and there is 32 bits vs 64 bits).

At last, a user-land x86 program for Windows is some executable which won't work on the same x86 architecture (or even the same hardware) running some other operating system (e.g. Linux), because the ABI, the system calls, the executable format (PE on Windows, ELF on Linux) are different. Read Operating Systems: Three Easy Pieces for more about OSes.

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • Your answer doesn't really answer OPs question. If it does anything at all, it causes him to have even more confusion. Perhaps you could add some text about the conditions under which a Windows program written in x86 assembly runs on others CPUs? – fuz Jun 18 '18 at 11:53
  • Does NASM work on Windows, or should I go with MASM? – securityauditor Jun 18 '18 at 11:58
  • I don't know, but probably yes. I never used Windows! – Basile Starynkevitch Jun 18 '18 at 12:00
  • 2
    @securityauditor NASM does work on Windows, see it's release folder https://www.nasm.us/pub/nasm/releasebuilds/2.14rc5/ – xmojmr Jun 18 '18 at 12:01
  • 2
    @securityauditor while NASM has binaries also for windows and does work there well, the whole windows world is often focused around MS dev tools, so using visual studio with MASM may be somewhat simpler to set up and use. Although as the ASM development is now so rare, it may turn out the tutorials/tools are not that friendly anymore, and actually setting up NASM + clang or gcc may be easier even at windows.. (I don't know, haven't seen windows for a decade, do your own research) And finally to me it's weird why even bother with old windows, as you can use any other modern OS for free. – Ped7g Jun 18 '18 at 12:02
  • 1
    @securityauditor - You don't say how you program on Windows right now, but if you happen to use Visual Studio for your C code, it has built in support for asm as well - nothing to install. [How to enable assembly language support in Visual Studio](https://stackoverflow.com/questions/32847699/how-to-enable-assembly-language-support-in-visual-studio-2015) – Bo Persson Jun 18 '18 at 12:58
  • @BoPersson Yes, but then my question becomes 1) What architecture asm do I write? 2) Provided I do nothing crazy will it work on the same computers as a bog standard C program as it seems like in Asm the prgogrammer has to create 9999 different versions of exactly the same program to support every CPU :'-( – securityauditor Jun 18 '18 at 13:28
  • 3
    @securityauditor - 1) Obviously this is an MS product, so they use MASM-format. 2) Going off-topic - you write code in assembly because you want to do something *very special* that the C compiler cannot or will not do for your specific CPU, like using some very specific instructions. So the code will not be portable anyway. Writing in assembly is somewhat "going crazy" to begin with. :-) – Bo Persson Jun 18 '18 at 13:50
  • @securityauditor while writing some real SW in assembly today is crazy (at least unless you show some C++ prototype and what kind of issue forces you to use ASM, which will be probably quickly resolved on the C++ side any way), LEARNING assembly is quite essential to have better understanding how computers and compilers works, and what kind of algorithms and data structures are computing-friendly. You will benefit from that also when writing code in higher-level programming languages. Also once you did learn to debug code in assembly, you will find debugging other problems much simpler... :D – Ped7g Jun 18 '18 at 14:26
0

Assembly language has many parts that work on every CPU, but there are certain things that are different for different CPUs, because assembly language communicates directly with a CPU (unlike high level programming languages). To understand why, one must understand CPUs.

CPUs have temporary storage that is used for processing. Some of those storages are for specific purposes, but most are used for anything. Different CPUs allow for different amounts of data to be stored in those containers. Furthermore, some CPUs have less storage containers than others. A x86 CPU does not have as many storage containers or processing power as an x64 CPU.

On a x64 CPU, one might be able to add 1084848 and 10487583848 (these numbers are completely random) directly, but one might have to break that problem up into multiple sections for x86.

On a x64 CPU, .long can store 8 bytes (64 bits), but on an x86 CPU, .long might not be able to store 8 bytes.

Furthermore, these all have exceptions, because not all x86 systems are the same. As stated in an above answer, some x86s use 16-bit, and some use both 16-bit and 32-bit.

You might want to learn more at: https://archive.org/details/ost-computer-science-programminggroundup-0-9/page/n26/mode/1up?view=theater And at: https://nixhacker.com/getting-processor-info-using-cpuid/amp/

I wouldn’t worry about it to much as long as it works on mine, because, technically, none of the systems compile the same, and many features don’t work on other systems. As said above, your C programs can’t run in every system. That is why Microsoft makes multiple versions and labels them for the operating systems they were meant for.

Perhaps you shouldn’t worry to much if your programs work on everything, unless you want to make multiple versions.

  • *A x64 CPU does not have as many storage containers or processing power as an x86 CPU.* - x86-64 CPUs have *more* and wider registers than x86 CPUs running in 32-bit mode. (That's why your example of adding a 64-bit integer would take more work on 32-bit x86, if you don't use MMX or SSE2.) Also, in GAS syntax, the `.long` directive is 4 bytes for both i386 and x86-64. It's actually just a synonym for `.int` - https://sourceware.org/binutils/docs/as/Long.html / https://sourceware.org/binutils/docs/as/Int.html - those do indeed vary by target, but i386 and x86-64 happen to agree. – Peter Cordes Mar 04 '23 at 20:15
  • Saying .long is the same thing as .int when I didn’t say they are different is completely useless. It is the same thing as saying I am correct. –  Mar 04 '23 at 20:36
  • By that reasoning, "2 might not equal 2" is technically a correct statement, but it's not useful. It would be much better to pick an example of something that *is* different in GAS syntax for 64 vs. 32-bit mode. But no examples come to mind for GAS directives; I think `.anything` is the same size for `as --32` vs. `as --64`. You might get link errors in 64-bit mode from using `.long foo` instead of `.quad foo` where `foo` is a symbol name, though; addresses are 64-bit. – Peter Cordes Mar 04 '23 at 20:54
  • Instead of editing to add *Also, I technically switched x86 and x64 around.* you can and should just fix the original paragraph to say what it always should have said, instead of confusing future readers until they read all the way to the end. The edit history is there for anyone to see if they want to look. – Peter Cordes Mar 04 '23 at 20:58