Hacker News new | past | comments | ask | show | jobs | submit login
Main is usually a function, so when is it not? (2015) (jroweboy.github.io)
136 points by phreack on Sept 10, 2017 | hide | past | favorite | 65 comments



Here is a link to the several times this has appeared here: https://news.ycombinator.com/from?site=jroweboy.github.io

I find these "hey there is this language called C and you can do some really weird stuff in it!" articles humorous because from the beginning of time there have been people who are fascinated by this aspect of the C language and gave rise to the whole obfuscated C contest thing.

The idea that a symbol references an address and its type is only a convenience in the source code always messes people up. When I've taught C to people there is always that one person who says "What if you cast a string pointer to a function and called it! Huh?" and I explain that is perfectly legal C and has been exploited for years and you can practically see their brain change conceptual planes in mid-air :-)


I like to start with the assembler, then just tell people that high level languages are fancy assemblers. Some of them restrict you to using the designer's favourite macros, some of them allow you to build on highly general tools at approximately the lowest level, but ultimately all computer programming languages are fancy macro assembler syntaxes.


This is an OK mental model until a wholly compliant C or C++ compiler blows it all to smithereens with an optimization exploiting UB.

You gotta write C or C++ for the abstract machine defined by the standards. Thinking about C in terms of some concrete assembly language or architecture can be problematic.


> "What if you cast a string pointer to a function and called it! Huh?" and I explain that is perfectly legal C

I would argue that it's less than "perfectly" legal: The C standard lists it as a "common extension":

  J.5.7 Function pointer casts 
  A pointer to an object or to void may be cast to a pointer to 
  a function, allowing data to be invoked as a function
And on a practical level, it seems to me that the increasing proliferation of NX enforcement is cramping the acceptance of this extension.


That's a good point, but the long running (although shrinking) gap between what the standard recommends, vs. what you can do with real compilers is one of the reasons that coding in C can be so error-prone.


The thing I'm wondering about with this is why ld by default creates only two LOAD segments, one r-x and one rw-. .text and .rodata are put into the former while .data is put into the latter. This is the only reason this works. So my question is why there isn't a third LOAD segment with access r-- for the read only data? If anything it would reduce the surface of where to find ROP gadgets slightly so couldn't hurt, no?


One part of the reason it's still this way is that read-only data is actually not read-only.

For example, say you have this:

const char* const errors[] = { "error 1", "error 2", /* ... */ };

Now, you'd expect all to be read-only, but with Position Independent Code (PIC) and Position Independent Executables (PIE), which are prerequisite for Address Space Layout Randomization (ASLR), it's not, because that errors array contains pointers to those literal strings, which means those pointers need to be "relocated": the executable or library can't contain the actual pointer to those, since their addess may vary between executions. So the executable/library only contains offsets, and the dynamic linker adjusts those offsets to make them real pointers. Thus that errors array is actually not read-only.

There is an additional ELF segment for those read-only-but-really-write-because-relocated data, GNU_RELRO, which tells the dynamic linker to remove the write bit on those parts of the read-write section where relocations happened.


I can't tell you why others don't do it, but OpenBSD has used a proper read-only rodata segment since long ago http://www.openbsd.org/papers/pacsec03/e/mgp00013.html


LLD (the new linker from the LLVM project) actually does exactly this; by default, it creates a separate read-only segment, and you have to pass --no-rosegment explicitly if you don't want this.

Amusingly enough, I ran into some issues because of this; e.g. valgrind's symbolication was failing on LLD-linked binaries unless I used --no-rosegment. I didn't dig into it too much, but it's probably making some bad assumptions about the text section's load address. (LLD places the read-executable segment after the read-only segment, and I think valgrind was assuming that the text section would be part of the first segment.)


That's architecture-specific. On some machines there is indeed a separate "execute-only" segment in the ELF file. But on many architectures (x86) there has historically been no hardware support for that, and for compatibility reasons the standard linker output still maintains the same scheme.


It's not a lack of execute-only segment but of a read-only, no-execute segment I'm talking about. x86 has had support for that for exactly as long as it has supported no-execute, which the second LOAD segment is marked with (i.e. the one that .data goes in).

Platform support is irrelevant, systems not supporting the access flags simply gives you more access. E.g. running a modern Linux on pre-Athlon 64 CPUs will just cause all readable pages to be executable as well.


The compatibility requirement is the result of the historic lack of x86 platform support, though: x86 used not to support r-- permissions, so it put rodata in an r-x segment, so there are likely programs in the wild which accidentally rely on that, so the linker can't now tighten the rodata permissions without breaking some existing set of programs of unknown size. Whether you think that's a good tradeoff depends on your opinion on the size of that set of programs and how heavily you weight 'avoid breaking code that used to work' against 'tighten permissions for security reasons'.

It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.


> x86 used not to support r-- permissions,

Depends on what you mean, the original page table entries have a R (Read/Write) flag, if not set the page is read-only. What you couldn't do was mark a page non-executable. But nevertheless the second LOAD segment is marked rw- and not rwx, so it would seem that it wasn't deemed a problem in the past having segments with unsupported permissions.

At the time when we got the NX bit it did happen that some programs broke because they expected executable data, but the security benefits were more important.

> It would be interesting to know if you can ask the linux linker to put rodata in its own r-- segment -- I scanned the docs but didn't see an option for it.

You have to write your own linker script, see e.g. [0].

[0] http://www.cl.cam.ac.uk/~srk31/blog/devel/custom-elf-phdrs.h...


By the r-- syntax I meant specifically 'readable, not writable, not executable' as distinct from rw- 'readable, writable, not executable' or r-x 'readable, not writable, executable'.


On i386 the descriptor cannot be both writable and executable at same time. But in order to support sane semantics for C, typical Unix OS (which for purposes of this discussion includes 32bit Windows) loads CS, DS and SS with different descriptor selectors that nevertheless alias to same range of linear addresses and thus essentially disable most of the MMU's protection logic and rely only on paging. And traditional 32bit i386 page table entries only have two flags: accessible at all (called "present") and writable.


Binutils has had support for it for as long as it's supported ELF output, which is to say many years longer than x86 has had the NX bit.

It's just not the default, for historical reasons.


The old a.out format used by BSD had a third, virtual section for uninitialized data. It was allocated at runtime but of course took no space in the object file.


Do you mean something different from ELF's bss segment?


It is different only in the sense that in ELF it is real segment (that even has sane ELF segment header when loaded), but in MZ, most flavours of a.out and some flavours of COFF it is just a single word quantity somewhere in the image header.


It is not really there in the ELF. It exists as a section. But sections are not used at runtime, they only exists for tools like debuggers etc. You can strip them from the binary with e.g. the sstrip[0] tool and it still works fine.

What is used at runtime is the program headers named LOAD, which specifies the segments. Here is an example:

    LOAD off    0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
         filesz 0x000000000000069c memsz 0x000000000000069c flags r-x
    LOAD off    0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**21
         filesz 0x0000000000000220 memsz 0x0000000000000650 flags rw-
...

    Idx Name          Size      VMA               LMA               File off  Algn
      25 .bss         00000420  0000000000601040 0000000000601040  00001030  2**5
                      ALLOC
So note that .bss is placed at the end of the second segment (.bss at 0x601040 + 0x420 = 0x601460, and the second segments ends in memory at 0x600e10 + 0x650 which is also 0x601460). But note also that the file size is only 0x220, which places the end of the data mapped from the file at 0x600e10 + 0x220 = 0x601030 which is slightly before .bss starts. So what happens is that the information about .bss is actually described by setting the memory size of the LOAD segment bigger than the file size, the dynamic linker will then fill the rest with zeroes.

[0] http://www.muppetlabs.com/~breadbox/software/elfkickers.html


I stand corrected about real linker behavior :) In fact merging all RW sections into one big segment with .bss at the end makes perfect sense.

My point was essentially that for ELF this is not some kind of kludgeish special case for .bdd, but general feature that any segment can be zero extended to arbitrary size larger than what is contained in the executable image (although on sane platforms it is not useful for anything but .bss)


On the subject of machine code as data, there was an interesting text file released at this year's SIGBOVIK: https://www.cs.cmu.edu/~tom7/abc/paper.txt (you'll need to resize your browser window to read it properly -- for an easier reading experience you can also check out the PDF and video at https://www.cs.cmu.edu/~tom7/abc/).


That is an amazing work of art. I only skimmed it briefly but it would make for great spare-time reading.



The program entry point is actually _start (which does some setup and later calls main()) so for even more extreme TA befuddlement, write a program that doesn't even call main()!


    int noMain()     __attribute__ (constructor);
    int noMain() {
        printf("Goodbye, main()");
    } // &etc.


This actually would not link, because _start() or something it calls into (depending on implementation of CRT on given platform) would contain unresolved reference to main. (and goven the fact that all this CRT startup code is usually one .o, you cannot just patch out the part that calls main(), you have to replace it completely)

On the other hand something around the lines of:

    void _start(){
      write(1, "Hello world\n", 12);
      exit(0);
    }
should probably work, given that you link it without CRT startup code (ie. gcc -nostdlib)


Now for more confusion, have a main function that _start doesn't call.

Now for even more confusion, have the main function (which never runs) call the _start function.


You are evil!


He should be writing some crackmes.


We joked with him about how he needs to make a program that works, but the grading TAs wouldn’t be able to figure out how it works.

Unless you come across a TA like me (I've been one before), who will comment on the fact that using xor+inc or push+pop would be shorter ways to set a register to a small immediate. ;-)


The CLR uses a similar technique to take over control of execution of a managed EXE - http://srevas.net/notes/2007/12/25/mscoree/


> Since I knew the target system is going to be 64bit Linux

Knowing the target system makes a lot of these things quite a bit easier. I had really good luck in college knowing our graphics teacher was using a 286.

As a TA you pretty much can be a hardass or save time for the rest of the semester and just mark 100.

Another early scenario was declaring main() as void in some embedded systems. I guess there was nothing to return to but it was still odd.


It's weird how compilers complain if main() is void, despite it not having to return anything if it's int.


Turbo C back in the day didn’t even give a warning.


You have to return something. If you don't, the exit code is the value that happened to be in memory at this point.


N1256 5.1.2.2.3 p1: If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0. If the return type is not compatible with int, the termination status returned to the host environment is unspecified.


main() is __cdecl__ or __stdcall__ on the major platforms.

That calling convention on x86 specifies that the return value is red from the EAX register. So, when your main() function exits, the return value is red from EAX, it's that simple.

The compilers may add boilerplate code around the main, in fact main() is rarely the real main() function, but that doesn't change the spec.


Did you read the comment you just replied to?!


The two comments are not incompatible, they are just very different worldviews. One tells you what the standard says (that the termination status is unspecified if main does not return an int), the other tells you what usually happens (you get what happened to be in AX).

And as I've just tested, gcc doesn't return zero termination status if you reach the } at the end of main.


Calling conventions are as much a standard as the C spec.

The main() is called like a regular function, by the system thing that executes programs.

Depending on the compiler and the flags, the main() is not the real entry point of the program. It can add another entry point to do some magic, like setting the return code.


Depends if it's in C99 mode. GCC defaults to “GNU89” I think.


Normally, yes. But main() is special and implicitly returns 0, for whatever reason.


Absolutely not. You can look at a crashing programs and see the return code is a random value. You can also make a program that doesn't return anything and see that it's also a random value, though some compilers on some platforms might initialize that to zero.


No, it's literally special-cased in the C standard.


Someone should tell the gcc maintainers then.

    int main()
    {
        int a = 12;
    }

    $ gcc maintest.c
    $ ./a.out
    $ echo $?
    148


  $ cat test.c
  int main(void) {int a = 12;}
  $ gcc-6 test.c -o test --std=c89; ./test; echo $?
  162
  $ gcc-6 test.c -o test --std=c99; ./test; echo $?
  0
Must be a C99 thing.


This isn't that conclusive of a test -- uninitialized memory is 0 relatively often.



Okay, that is conclusive. (And I'm pleasantly surprised by how readable GCC's source code is.)


This has nothing to do with memory. The x86_64 ABI returns values from functions in the RAX register.


Sure; same point applies though -- just because 0 happens to be in RAX doesn't mean it's defined behavior.


Just tested on the MSVC compiler I have at hands. The standard is not respected.


MSVC doesn't claim to implement C99, where this rule was added, so it kind of makes sense that it doesn't happen. (generally, if they added C99-stuff then only where it was required for C++)


    int main(void)
    {
        __asm {
          mov eax, 123
        }
    }

    // visual studio command line
    // cl testc.c
    // cl testcpp.cpp
    // testc.exe
    // echo C %errorlevel%
    // testcpp.exe
    // echo CPP %errorlevel%
Indeed. It's a C99 thing.

For those not familiar with MSVC. The C compiler is not C99 compliant and the C++ compiler is.


I love C but this whole thread highlights a lot of the criticism of it. Everyone's right based on some standard or switch, and everyone's wrong for the same reason.


This is not specific to C, but common to all evolving standards. If you want portable code just drive on the middle of the road.

Often this is as easy as coding to an older standard, like C89 with a few selected features from later standards, and adding some compatibility features / problem detection in the build system.

And don't rely on features that are difficult to explain and/or add only questionable value. Just return 0 from main, it's the obvious simple thing to do.


Unless the assignment in question specifies architecture, the TA could (should? I know I would) run it on a 32 bit Windows environment and mark it down for not working.


Not surprisingly, there are tricks one can use to figure out what the environment is, using the same binary; start here:

https://stackoverflow.com/questions/38063529/x86-32-x86-64-p...

...and if you want to expand it to run on a SPARC or MIPS or something else...

https://hackaday.io/project/18614-polyglot-one-binary-multip...

The general idea is to find a set of bytes which can be interpreted in different (and valid) ways by all the architectures you want to support, and using those differences, jump to architecture-specific code.


Is it undefined behavior to have main be anything other than a function that returns int?


Yes, in hosted environment it is UB, unless the implementation specifies otherwise. Relevant quotes from N1256:

1. In this International Standard, "shall" is to be interpreted as a requirement on an implementation or on a program;

2. If a "shall" or "shall not" requirement that appears outside of a constraint is violated, the behavior is undefined.

3. (5.1.2.2) A hosted environment need not be provided, but shall conform to the following specifications if present.

4. (5.1.2.2.1) The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:

    int main(void) { /* ... */ }
or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):

    int main(int argc, char *argv[]) { /* ... */ }
or equivalent; or in some other implementation-defined manner.

5. (J.2 Undefined behavior) - A program in a hosted environment does not define a function named main using one of the specified forms (5.1.2.2.1).


UB comes with high-level languages. "main" is not specified in the high-level language, but by the OS. You write some machine code, give it the name "main" and the OS jumps to that location and starts executing. One should adhere to the "C calling convention" (managing the stack correctly) if that code is expected to behave within the system.

But "main is not a function" does not produce undefined behavior.


> "main" is not specified in the high-level language, but by the OS. You write some machine code, give it the name "main" and the OS jumps to that location

main() is specified by C. The OS as such doesn't know or care about main. Headers in the binary executable specify where the OS should begin execution, and this is rarely in main.


If you're feeding source code to a C compiler, it makes sense to ask whether or not it contains UB.


ah, this reminded me of some embeded oses where driver came as precompiled arrayblobs, that are included with ifdef guards set in some central config tool




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: