C - print args without stdlibs


C - print args without stdlibs



I just wrote a C program that prints its command line argument without using the standard library or a main() function. My motivation is simply curiosity and to understand how to play with inline assembly. I am using Ubuntu 17.10 x86_64 with the 4.13.0-39-generic kernel and GCC 7.2.0.


main()



Below is my code which I have tried to comment as much as I understood. The functions print, print_1, my_exit, and _start() are required by the system to run the executable. Actually, without _start() the linker will emit a warning and the program will segfault.


print


print_1


my_exit


_start()


_start()



Functions print and print_1 are different. The first one prints out a string to the console, measuring the length of the string internally. The second function needs the string length passed as an argument. The my_exit() function just exits the program, returning the required value, which in my case is the string length or the number of command line arguments.


print


print_1


my_exit()



print_1 requires the string length as an argument so the characters are counted with a while() loop and the length is stored in strLength. In this case everything works pretty well.


print_1


while()


strLength



Strange things happen when I use the print function, which measures the string length internally. Simply speaking, it looks like this function somehow changes the string pointer to point to environment variables which should be the next pointer and instead of the first argument the function prints "CLUTTER_IM_MODULE=xim", which is my first environment variable. My workaround is to assign *a to *b in the next line.


print


"CLUTTER_IM_MODULE=xim"


*a


*b



I couldn't find any explanation inside the counting procedure, but it looks like it's changing my string pointer.


unsigned long long print(char * str){
unsigned long long ret;
__asm__(
"pushq %%rbx nt"
"pushq %%rcx nt" //RBX and RCX to the stack for further restoration
"movq %1, %%rdi nt" //pointer to string (char * str) into RDI for SCASB instruction
"movq %%rdi, %%rbx nt" //saving RDI in RBX for final substraction
"xor %%al, %%al nt" //zeroing AL for SCASB comparing
"movq $0xffffffff, %%rcx nt" //max string length for REPNE instruction
"repne scasb nt" //counting "loop" see details: https://www.felixcloutier.com/x86/index.html for REPNE and SCASB instructions
"sub %%rbx, %%rdi nt" //final substraction
"movq %%rdi, %%rdx nt" //string length for write syscall
"movq %%rdi, %0 nt" //string length into ret to return from print
"popq %%rcx nt"
"popq %%rbx nt" //RBX and RCX restoration

"movq $1, %%rax nt" //write - 1 for syscall
"movq $1, %%rdi nt" //destination pointer for string operations $1 - stdout
"movq %1, %%rsi nt" //source string pointer
"syscall nt"
: "=g"(ret)
: "g"(str)
);
return ret; }

void print_1(char * str, int l){
int ret = 0;

__asm__("movq $1, %%rax nt" //write - 1 for syscall
"movq $1, %%rdi nt" //destination pointer for string operations
"movq %1, %%rsi nt" //source pointer for string operations
"movl %2, %%edx nt" //string length
"syscall"
: "=g"(ret)
: "g"(str), "g" (l));}


void my_exit(unsigned long long ex){
int ret = 0;
__asm__("movq $60, %%raxnt" //syscall 60 - exit
"movq %1, %%rdint" //return value
"syscallnt"
"ret"
: "=g"(ret)
: "g"(ex)
);}

void _start(){

register int ac __asm__("%rsi"); // in absence of main() argc seems to be placed in rsi register
//int acp = ac;
unsigned long long strLength;
if(ac > 1){
register unsigned long long * arg __asm__("%rsp"); //argv array
char * a = (void*)*(arg + 7); //pointer to argv[1]
char * b = a; //work around for print function
/*version with print_1 and while() loop for counting
unsigned long long strLength = 0;
while(*(a + strLength)) strLength++;
print_1(a, strLength);
print_1("n", 1);
*/
strLength = print(b);
print("n");
}
//my_exit(acp); //echo $? prints argc
my_exit(strLength); //echo $? prints string length}





That's not how inline assembly works. You should create standalone assembly code instead. Anyway, I don't understand what you are asking exactly.
– Jester
Jul 1 at 22:27





He said he was doing it to learn inline assembly, so it is clear he was trying to replace as much of the C code as he could.I never said anything about it being a good way to learn inline, or that everything should be made inline, but you questioned why he did it this way, and as I said he stated it in his first paragraph. I didn't question your commentary about the clobbers. There is a whole lot more wrong than clobbers. str may not be realized in memory and the redzone hasn't been accounted for unless he has turned it off as a compile option
– Michael Petch
Jul 1 at 23:05


str





register unsigned long long * arg __asm__("%rsp") and register int ac __asm__("%rsi"); are used in a way that is against what the documentation describes.since it isn't being used as an operand to an extended inline assembly template.
– Michael Petch
Jul 1 at 23:07



register unsigned long long * arg __asm__("%rsp")


register int ac __asm__("%rsi");





Jester offered the best solution. Use standalone assembly code in an assembly file. With that being said it is possible (convoluted) to do what you want with a combination of basic asm and extended asm templates. basic asm at global scope to create the _start function since you need to avoid all the potential compiler generated code a real C function would add.
– Michael Petch
Jul 1 at 23:15



_start





"//destination pointer for string operations $1 - stdout" is a weird comment. write() doesn't use C strings, it uses an explicit-length buffers. I'd comment that line with output fd = 1 = stdout. (I normally use "destination" to describe a pointer or something inside your own program, not an external output.) See the comments in Printing an integer as a string with AT&T syntax, with Linux system calls instead of printf for an example of nice commenting, IMO. Maybe overly verbose.
– Peter Cordes
Jul 2 at 0:08


//destination pointer for string operations $1 - stdout


write()


output fd = 1 = stdout




1 Answer
1



char * a = (void*)*(arg + 7); is completely a "happens to work" thing, if it works at all. Unless you're writing __attribute__((naked)) functions that only use inline asm, it's completely up to the compiler how it lays out stack memory. It appears that you are getting rsp, although that's not guaranteed for this unsupported use of a register-asm local. (Using the requested register is only guaranteed when used as an operand to an inline asm statement.)


char * a = (void*)*(arg + 7);


__attribute__((naked))


rsp



If you compile with optimization disabled, gcc will reserve stack slots for locals so char * b = a; makes gcc adjust RSP by more on function entry, so that's why your hack happens to change gcc's code-gen to match the hard-coded +7 (times 8 bytes) offset you put in the source.


char * b = a;


+7



On entry to _start, the stack contents are: argc at (%rsp), argv starting at 8(%rsp). Above the terminating NULL pointer for argv, the envp array is also in stack memory. So that's why you get CLUTTER_IM_MODULE=xim when your hard-coded offset gets the wrong stack slot.


_start


argc


(%rsp)


argv


8(%rsp)


envp


CLUTTER_IM_MODULE=xim



// in absence of main() argc seems to be placed in rsi register


// in absence of main() argc seems to be placed in rsi register



That's probably left over from the dynamic linker (which runs in your process before _start). If you compiled with gcc -static -nostdlib -fno-pie, your _start would be the real process entry point reached directly from the kernel, with all registers = 0 (except RSP). Note that the ABI says undefined; Linux chooses to zero them to avoid information leaks.


_start


gcc -static -nostdlib -fno-pie


_start



You can write a void _start(){} in GNU C that works reliably with and without optimization enabled, and works for the right reasons, with no inline asm (but still dependent on the x86-64 SysV ABI's calling convention and process-entry stack layout). No hard-coding of offsets that happen to occur in gcc's code-gen is needed. How Get arguments value using inline assembly in C without Glibc?. It uses stuff like int argc = (int)__builtin_return_address(0); because _start isn't a function: the first thing on the stack is argc rather than a return address. It's not pretty and not recommended, but given the calling convention that's how you can get gcc to generate code that knows where things are.


void _start(){}


int argc = (int)__builtin_return_address(0);


_start



Your code clobbers registers without telling the compiler about it. Everything about this code is nasty, and there's no reason to expect any of it to work consistently. And if it does, it's by chance and could will break with different surrounding code or compiler options. If you want to write whole functions, do it in stand-alone asm (or in inline asm at global scope) and declare a C prototype so the compiler can call it.



Look at gcc's asm output to see what it generated around your code. (e.g. put your code on http://godbolt.org/). You'll probably see it using registers that you clobbered in your asm. (Unless you compiled with optimization disabled, in which case it doesn't keep anything in registers between C statements to support consistent debugging. Only clobbering RSP or RBP would cause problems; other inline asm clobber bugs would go undetected.) But clobbering the red zone would still be a problem.



See also https://stackoverflow.com/tags/inline-assembly/info for links to guides and tutorials.



The right way to use inline asm (if there is a right way) is normally to let the compiler do as much as possible. So to make a write system call, you'd do everything with input / output constraints, and the only instruction inside the asm template would be "syscall", like this nice example my_write function: How to invoke a system call via sysenter in inline assembly? (The actual answer has 32-bit int $0x80 and x86-64 syscall, but not an inline asm version using 32-bit sysenter because that's not a guaranteed-stable ABI).


"syscall"


my_write


int $0x80


syscall


sysenter



See also What is the difference between 'asm', '__asm' and '__asm__'? for another example.



https://gcc.gnu.org/wiki/DontUseInlineAsm for lots of reasons why you shouldn't use it (like defeating constant-propagation and other optimizations).



Beware that a pointer input constraint for an inline asm statement does not imply that the pointed-to memory is also an input or output. Use a "memory" clobber, or see at&t asm inline c++ problem for a dummy operand workaround.


"memory"






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Rothschild family

Cinema of Italy