4 ms·
Hello world in C inline assembly (2018)
- onderweg 2y agoIs anyone aware of a similar example, for ARM assembly on macOS?
- brontitall 2y agoNot inline, but this was linked in a comment on HN a few days ago https://github.com/below/HelloSilicon https://github.com/below/HelloSilicon
- oguz-ismail 2y agoint 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) ); }
- rep_lodsb 2y agoActually 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 2y agoThanks 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 2y agoI 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 2y agoYou might want to compare it to the "proper" version of the inline asm code, from this comment: https://news.ycombinator.com/item?id=40703314 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 2y agoI 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 2y agoThat isn't proper C, rather GCC and clang dialects of inline Assembly.
- im3w1l 2y agoNever 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 2y agoTried 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 2y agoNow that's incredibly cursed. Could do basically anything and swallows the error too!
- gpderetta 2y agoI 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 2y agoIt'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.
- msla 2y agoWhen 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 2y agoWith 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 2y agoThe 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 2y agoAs 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 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 2y agoOr 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 2y agoYou 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 https://justine.lol/dox/rmsiface.txt
- jart 2y agoGot 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 2y agoIf you ever feel the need to do this in production, use linux_syscall_support.h (LSS) https://chromium.googlesource.com/linux-syscall-support 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 2y ago> 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 2y agoYou 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.S https://github.com/fsmv/dfre/blob/master/code/linux32_start.... This is for -nostdlib not -ffreestanding
- sp1rit 2y agoYou can usually with not having the initial part. As long as you do call the exit syscall, it should work.
- ASalazarMX 2y ago"This C program doesn’t explicitly use any C standard library functions." doesn't sound as cool, though.
- layer8 2y agoThe `return 0;` is optional for main() in C, so the function body could be made to consist solely of inline assembly.
- _8j50 2y agoTry this with visual studio and x64. Microsoft!!