[HN Gopher] Hello world in C inline assembly (2020)
___________________________________________________________________
Hello world in C inline assembly (2020)
Author : aragonite
Score : 129 points
Date : 2024-06-16 06:04 UTC (1 days ago)
(HTM) web link (jameshfisher.com)
(TXT) w3m dump (jameshfisher.com)
| onderweg wrote:
| Is anyone aware of a similar example, for ARM assembly on macOS?
| brontitall wrote:
| Not inline, but this was linked in a comment on HN a few days
| ago
|
| https://github.com/below/HelloSilicon
| oguz-ismail wrote:
| int main(void) { register const char *msg
| asm("x1") = "hello, world!\n"; asm (
| "mov w0, #1\n" "mov w2, #14\n"
| "mov w16, #4\n" "svc #128\n" :
| : "r" (msg) ); }
| robxorb wrote:
| How does one compile this?
|
| EDIT: my bad, my source had a typo - it's as easy as you'd
| think: $ cc hello.c -o hello $ ./hello
| onderweg wrote:
| thanks!
| rep_lodsb wrote:
| Actually more readable than the AT&T syntax :)
|
| But does this work on both GCC and Clang, and is safe from being
| optimized away? _edit: the answer is no_
|
| Turbo Pascal had an integrated assembler that could use symbols
| (and even complex types) defined anywhere in the program, like
| this: procedure HelloWorld; assembler;
| const Message: String = 'Hello, world!'^M^J; {Msg+CR+LF}
| asm mov ah,$40 {DOS system call number for write}
| mov bx,1 {standard output} xor ch,ch {clear
| high byte of length} mov cl,Message.byte[0]
| mov dx,offset Message+1 int $21 end;
| boffinAudio wrote:
| Thanks for making me extremely sentimental for the hundreds of
| Turbo Pascal projects I did back in the day - this particular
| example highlights the elegance and clarity of the language,
| which we still seem to resist in our modern tooling.
| g15jv2dp wrote:
| I don't really see what's "elegant" about the code, could you
| elaborate? (This isn't a jab at GP. I'm just curious about
| what I'm not seeing.)
| rep_lodsb wrote:
| You might want to compare it to the "proper" version of the
| inline asm code, from this comment:
| https://news.ycombinator.com/item?id=40703314
|
| Modern C is neither "low-level" or "high-level". It's
| defined for an abstract machine where integers can't
| overflow, null pointers can't be referenced, etc. And
| unless you follow all the rules, and add proper annotations
| for things like inline assembly, the compiler is free to do
| _anything_ to your code.
|
| The one advantage to this approach is that modern compilers
| can turn megabytes of auto-generated crap produced by
| string substitution macros into halfway decent machine
| language.
|
| (And I freely admit that specifically Turbo Pascal produced
| _really_ bad code, worse even than C compilers at the time,
| but the syntax is oh so much nicer IMHO)
| gpderetta wrote:
| I believe that MSVC inline asm allows referencing
| variables in the asm as it can parse and understand the
| asm (at least before they got rid of inline asm
| completely for 64 bit code).
|
| AFAIK GCC does not attempt to parse the asm by design, as
| it is meant to be used for code that the compiler might
| not understand, so you have to describe input, outputs
| and side effects with annotations.
| pjmlp wrote:
| That isn't proper C, rather GCC and clang dialects of
| inline Assembly.
| boffinAudio wrote:
| I think its elegant because the distinction between Pascal
| and Assembly is made using the Pascal _asm .. end;_
| keywords, and in that block one can also access the Pascal
| variables without much fuss involving the assembler.
|
| I find that really nice to read and to look at, whereas the
| examples given in the original article are prone to syntax
| overload, what with all the intermixing - for example, the
| variable declarations having what 'look' like attributes -
| but are really assembly instructions, emitted.
|
| I guess one would have had to have enjoyed writing Turbo
| Pascal code, though, to see this particular aesthetic. A
| lot of folks do, some don't ..
| pjmlp wrote:
| Not only Turbo Pascal, this more sane approach to inline
| Assembly was quite common in the PC world compilers, regardless
| of the programming language.
| pajko wrote:
| Inline assembly also has support for symbol names, although the
| native symbols could not be accessed directly but in a bit
| awkward way.
|
| https://stackoverflow.com/questions/32131950/assembler-templ...
| im3w1l wrote:
| Never seen inline assembly written quite like that, is this
| actually correct code? I'm concerned that normally register
| annotation is just a hint, and that the assembly blocks are not
| marked volatile - and that the compiler may therefore be free to
| rewrite this code in many breaking ways.
|
| Edit: Ah a basic asm blocks is implicitly volatile. I'm still a
| little concerned the compiler could get clever and decide the
| register variables are unused and optimize them out.
| rep_lodsb wrote:
| Tried it with GCC, and without any optimization it does print
| the message. With "-O2" however, we get this:
| Disassembly of section .text: 0000000000001040
| <main>: 1040: 0f 05 syscall
| 1042: 31 c0 xor %eax,%eax 1044:
| c3 retq
|
| Everything _except_ the syscall instruction has been optimized
| away!
| im3w1l wrote:
| Now that's incredibly cursed. Could do basically anything and
| swallows the error too!
| gpderetta wrote:
| I think that named register variables (a GCC extension) are
| meant to be live in asm block by design, so they shouldn't be
| optimized away.
|
| Still I would use extended asm.
|
| edit: from the docs: "The only supported use for [Specifying
| Registers for Local Variables] is to specify registers for
| input and output operands when calling Extended asm".
|
| So the example is UB.
| bregma wrote:
| It's not UB, it's documented behaviour of a vendor extension.
|
| It's not UB because it's defined as outside the scope of the
| language standard. The vendor (in this case, GCC) does
| document how to use its inline assembly extension in quite a
| lot of detail, including how to use clobber lists to prevent
| exactly the kind of thing these failures demonstrate.
| gpderetta wrote:
| GCC says that register extensions are not supported with
| basic asm. What happens if you do it anyway is not
| documented. Ergo UB.
| bregma wrote:
| GCC says that register extensions are not supported with
| basic inline asm. If you do it anyway it doesn't work
| it's not undefined behaviour, it's behaving as
| documented. Once you've ventured into vendorland you're
| outside the realm of undefined behaviour to start with,
| but following the vendor's documentation on how to use
| the vendor's extension is the minimum requirement for
| meeting your expectations that the feature will work.
| gpderetta wrote:
| > it's behaving as documented.
|
| what do you think the documented behaviour is? None is
| documented, so it is undefined. The only defined
| behaviour is with extended asm.
|
| Remember in C and C++ everything is UB unless defined in
| the standard or by an implementation.
|
| If it was documented as giving an hard error, you would
| be right. But it is not.
| im3w1l wrote:
| I don't think this is exactly correct. Undefined behavior
| means a very specific thing - that the program could do
| literally anything. But I think that's not quite the
| situation in this case. Rather, I would suspect that
| these specified-register variables are only guaranteed to
| be effective with extended asm constraints.
|
| For basic asm I would assume then that the register
| contents cannot be relied on to contain the value of the
| variable, but as long as you don't rely on it, then you
| are in the clear.
|
| Then again it's hard to be sure about these matters with
| C.
| msla wrote:
| When I compile it with GCC 12, this machine code results:
| 1129: f3 0f 1e fa endbr64 112d:
| 55 push rbp 112e: 48 89 e5
| mov rbp,rsp 1131: b8 01 00 00 00 mov
| eax,0x1 1136: bf 01 00 00 00 mov
| edi,0x1 113b: 48 8d 05 c2 0e 00 00 lea
| rax,[rip+0xec2] # 2004 <_IO_stdin_used+0x4> 1142:
| 48 89 c6 mov rsi,rax 1145: ba 0f
| 00 00 00 mov edx,0xf 114a: 0f 05
| syscall 114c: b8 00 00 00 00 mov
| eax,0x0 1151: 5d pop rbp
| 1152: c3 ret
|
| Can you spot the error?
|
| . . . . . .
|
| The code biffs rax when it loads the string address, so the
| system call number is lost, and the code ends up not printing
| anything. Moving the string assignment to be the very first line
| in main fixes it.
|
| BTW, Clang 14 with no optimization accepts the code without issue
| but compiles it without using any of the registers; it just
| stores the values to memory locations and runs the syscall
| opcode. With O1 optimization or higher, it optimizes away
| everything except the syscall opcode.
| rep_lodsb wrote:
| With an older version, it works (as long as there is no
| optimization at least, with -O2 all the register init code
| disappears):
|
| $ gcc -v
|
| ... gcc version 10.2.1 20210110 (Debian 10.2.1-6)
| 0000000000001125 <main>: 1125: 55
| push %rbp 1126: 48 89 e5 mov
| %rsp,%rbp 1129: b8 01 00 00 00 mov
| $0x1,%eax 112e: bf 01 00 00 00 mov
| $0x1,%edi 1133: 48 8d 35 ca 0e 00 00 lea
| 0xeca(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
| 113a: ba 0e 00 00 00 mov $0xe,%edx 113f:
| 0f 05 syscall
|
| No idea why a newer version produces worse code in this case
| (though of course, this way of doing inline assembly isn't
| "correct" anyway, so nasal demons may result)
| khrbtxyz wrote:
| The exact same thing happens with GCC 12 with 32-bit MIPS.
| #include <asm/unistd.h> char msg[] = "hello,
| world!\n"; int main(void) {
| register int syscall_no asm("v0") = __NR_write;
| register int arg1 asm("a0") = 1; register char
| *arg2 asm("a1") = msg; register int arg3
| asm("a2") = sizeof(msg) - 1; asm("syscall");
| return 0; } root@OpenWrt:~# objdump
| --disassemble=main ... 00400580 <main>:
| 400580: 27bdfff8 addiu sp,sp,-8 400584: afbe0004 sw
| s8,4(sp) 400588: 03a0f025 move s8,sp 40058c:
| 24020fa4 li v0,4004 400590: 24040001 li a0,1
| 400594: 3c020041 lui v0,0x41 400598: 24450650 addiu
| a1,v0,1616 40059c: 2406000e li a2,14 4005a0:
| 0000000c syscall
| jmillikin wrote:
| As other comments have noted, the asm statement needs to have its
| input/output registers specified to ensure the compiler doesn't
| erase the "unused" values.
|
| Working example: https://john-millikin.com/unix-
| syscalls#linux-x86-64-gnu-c
|
| Adapted to use main(): static const int STDOUT =
| 1; static const int SYSCALL_WRITE = 1; static const
| char message[] = "Hello, world!\n"; static const int
| message_len = sizeof(message); int main() {
| register int rax __asm__ ("rax") = SYSCALL_WRITE;
| register int rdi __asm__ ("rdi") = STDOUT;
| register const char *rsi __asm__ ("rsi") = message;
| register int rdx __asm__ ("rdx") = message_len;
| __asm__ __volatile__ ("syscall" : "+r" (rax) :
| "r" (rax), "r" (rdi), "r" (rsi), "r" (rdx) : "rcx",
| "r11"); return 0; }
|
| Test with: $ gcc -o hello hello.c $ ./hello
| Hello, world!
| im3w1l wrote:
| Or just int main(void) { asm
| volatile("syscall" : : "a"(1), "d"(14), "D"(1), "S"("hello
| world!\n")); return 0; }
|
| Though the clobber list is weak spot, I don't know exactly what
| it should have in this case.
| jart wrote:
| You want: long ax; asm
| volatile("syscall" : "=a"(ax) : "0"(1), "D"(1), "S"("hello
| world!\n"), "d"(14));
|
| You can also say: long ax = 1; asm
| volatile("syscall" : "+a"(ax) : "D"(1), "S"("hello
| world!\n"), "d"(14));
|
| https://justine.lol/dox/rmsiface.txt
| jart wrote:
| Got some sleep and took a second look. You actually want:
| long ax = 1; asm volatile("syscall" : "+a"(ax) :
| "D"(1), "S"("hello world!\n"), "d"(14) : "rcx", "r11");
|
| Sorry folks! Note also this only works on Linux. On BSDs
| for example, even if you change the magic number, BSDs may
| clobber all the call-clobbered registers. So with those
| OSes it's usually simplest to write an assembly stub like
| this: my_write: mov $4,%eax
| syscall ret
| Retr0id wrote:
| If you ever feel the need to do this in production, use
| linux_syscall_support.h (LSS)
| https://chromium.googlesource.com/linux-syscall-support
|
| No need to remember syscall numbers or calling conventions, or
| the correct way to annotate your __asm__ directives, and it's
| even cross-architecture.
| sp1rit wrote:
| > This C program doesn't use any C standard library functions.
|
| This is only half true. While the code doesn't call any stdlib
| functions, it still relies on the the c stdlib and runtime in
| order to get called and properly exit.
|
| I'm somewhat perplexed why the author did do it with the runtime,
| given that he doesn't really depend on features of it (except
| maybe the automatic exit code handling) instead of building with
| -ffreestanding.
| fsmv wrote:
| You have to add some extra assembly before main if you don't
| use the C runtime. You have to write _start, the actual entry
| point that CRT usually takes.
| https://github.com/fsmv/dfre/blob/master/code/linux32_start....
|
| This is for -nostdlib not -ffreestanding
| sp1rit wrote:
| You can usually with not having the initial part. As long as
| you do call the exit syscall, it should work.
| ASalazarMX wrote:
| "This C program doesn't explicitly use any C standard library
| functions." doesn't sound as cool, though.
| layer8 wrote:
| The `return 0;` is optional for _main()_ in C, so the function
| body could be made to consist solely of inline assembly.
___________________________________________________________________
(page generated 2024-06-17 23:01 UTC)