问题
When I compile my c source code (for example in a Linux environment) the compiler generates a file in a "machine readable" format.
- Why the same file is not working on the same machine under a different operating system?
- Is the problem about the way we "execute" this file?
回答1:
Sometimes it will work, depending on the format and the libraries that you use, etc.. For example, things like allocating memory or creating a window all call the OS functions. So you have to compile for the target OS, with those libraries linked in (statically or dynamically).
However, the instructions themselves are the same. So, if your program doesn't use any of the OS functions (no standard or any other library), you could run it on another OS. The second thing that is problematic here is executable formats.. Windows .exe
is very different from for example ELF. However, a flat format that just has the instructions (such as .com
) would work on all systems.
EDIT: A fun experiment would be to compile some functions to a flat format (just the instructions) on one OS (e.g. Windows). For example:
int add(int x, int y) { return x + y; }
Save just the instructions to a file, without any relocation or other staging info. Then, on a different OS (e.g. Linux) compile a full program that will do something like this:
typedef int (*PFUNC)(int, int); // pointer to a function like our add one
PFUNC p = malloc(200); // make sure you have enough space.
FILE *f = fopen("add.com", "rb");
fread(p, 200, 1, f); // Load the file contents into p
fclose(f);
int ten = p(4, 6);
For this to work, you'd also need to tell the OS/Compiler that you want to be able to execute allocated memory, which I'm not sure how to do, but I know can be done.
回答2:
I have been asked what is an ABI discrepancy. I think it's best to explain over a simple example.
Consider a little silly function:
int f(int a, int b, int (*g)(int, int))
{
return g(a * 2, b * 3) * 4;
}
Compile it for x64/Windows and for x64/Linux.
For x64/Windows the compiler emits something like:
f:
sub rsp,28h
lea edx,[rdx+rdx*2]
add ecx,ecx
call r8
shl eax,2
add rsp,28h
ret
For x64/Linux, something like:
f:
sub $0x8,%rsp
lea (%rsi,%rsi,2),%esi
add %edi,%edi
callq *%rdx
add $0x8,%rsp
shl $0x2,%eax
retq
Allowing for different traditional notations of assembly language on Windows and Linux, there obviously are substantial differences in the code.
The Windows version clearly expects a
to arrive in ECX
(lower half of the RCX
register), b
in EDX
(lower half of the RDX
register), and g
in the R8
register. This is mandated by the x64/Windows calling convention, which is a part of the ABI (application binary interface). The code also prepares arguments to g
in ECX
and EDX
.
The Linux version expects a
in EDI
(the lower half of the RDI
register), b
in ESI
(the lower half of the RSI
register), and g
in the RDX
register. This is mandated by the calling convention of System V AMD64 ABI (used on Linux and other Unix-like operating systems on x64). The code prepares arguments to g
in EDI
and ESI
.
Now imagine that we run a Windows program which somehow extracts the body of f
from a Linux-targeted module and calls it:
int g(int a, int b);
typedef int (*G)(int, int);
typedef int (*F)(int, int, G);
F f = (F) load_linux_module_and_get_symbol("module.so", "f");
int result = f(3, 4, &g);
What is going to happen? Since on Windows functions expect their arguments in ECX
, EDX
and R8
, the compiler will place actual arguments in those registers:
mov edx,4
lea r8,[g]
lea ecx,[rdx-1]
call qword ptr [f1]
But the Linux-targeted version of f
looks for values elsewhere. In particular, it is looking for the address of g
in RDX
. We have just initialized its lower half to 4, so there are practically nil chances that RDX
will contain anything making sense. The program will most likely crash.
Running Windows-targeted code on a Linux system will produce the same effect.
Thus, we cannot run 'foreign' code but with a thunk. A thunk is a piece of low-level code which rearranges arguments to allow calls between pieces of code following different sets of rules. (Thunks may probably do something else because the effects of ABI may not be limited by the calling convention.) You typically cannot write a thunk in high-level programming language.
Note that in our scenario we need to provide thunks for both f
('host-to-foreign') and g
('foreign-to-host').
回答3:
There are two things of importance:
- the development environment;
- the target platform.
The development environment's compiler generates an object file with machine code and references to functions and data not contained in the object moule (not defined in the source file). Another program, the linker, combines all your object modules, plus libraries, into the executable. Please note:
The format of the object module is in principle platform Independent, although standards exist for platforms to easily combine object modules produced by different compilers for the platform. But that doesn't need to be; a fully integerated development environment can have its own "standard".
The linker can be a program from any manufacturer. It needs to know the format of the object modules, the format of the libraries and the desired format of the resulting excutable. Only this latter format is platform dependent.
The libraries can be in any format, as long as there is a linker that can read them. BUT: the libraries are platform dependent as the functions in the library call the API of the operating system.
A cross-development environment could for example generate object modules that are Windows compatible, then a linker can link them with libraries in Windows compatible format, yet targeted for Linux (using Linux OS calls) and deliver a Linux executable. Or any combination you like (Linux object format, windows library format, Windows executable; ...).
To summarize, the only truly platform dependent items are the functions in the libraries, as these call the OS, and the resulting executable as that is what the OS will load.
So, to answer the question: no, there is not necessarily a need to compile a source file for different platforms. The same object module can be linked for Linux (using Linux targeted libraries and creating a linux-format executable), or for Windows (using Windows targeted libraries and creating a Windows-format executable).
回答4:
- Different operating systems will use different Application Binary Interfaces (ABIs), this includes code needed for function entry and exit
- Certain language features may need direct platform support (things like thread local storage come to mind)
- The linker will generally link automatically to the toolchain specific standard library. This will need to change between Operating systems is for no other reason that each operating system has its own set of system calls.
Having said that, the Wine project is a good example where all these issue have been wrapped up to try to make windows code run on linux.
回答5:
You are right, compiling translates your source code into machine readable code, e.g. into x86 machine code.
But there is more to it than that. Your code often not only uses machine code that is compiled into your executable file, but also references operating system libraries. All modern operating systems supply different APIs and libraries to the programs. So if your program is built to work with e.g. some Linux libaries and is then executed under an operation system that doesn't contain these libraries it will not run.
The other thing here is the executable file format. Most executable files contain more than just executable machine code, but also some metadata, e.g. icons, information about how the file is packed, version numbers and quite a bit more. So by default, if you run e.g. a Windows .exe file on Linux, the operating system would not be able to handle that different file format correctly.
Systems like Wine add the missing libraries and are able to handle the different executable file formats, thus allowing you to run e.g. a Windows .exe file on Linux as if it was run on Windows natively.
回答6:
There are several good, general answers here. I'll give you a very specific example.
A x86 machine can easily run printf("Hello world")
both 32bit Linux and DOS, if the C file is compiled for each platform.
One of many major differences between operating systems is how a program instructs the operating system to provide the services it does. Here is how you ask Linux to print a string:
msg db "Hello world" # Define a message with no terminator
mov edx, 11 # Put the message length in the edx register
mov ecx, msg # Put the message address in ecx
mov ebx, 1 # Put the file descriptor in ebx (1 meaning standard output)
mov eax, 4 # Set the system call to 4, "write to file descriptor"
int 80h # Invoke interrupt 80h to give control to Linux
Here is how you ask DOS to print the same string:
msg db "Hello world$" # Define a message terminated by a dollar sign
mov dx, msg # Load the message address into dx
mov ah, 9 # Set the system call number to 9, "print string"
int 21h # Invoke interrupt 21h to give control to DOS
They both use the same kind of basic, machine readable and executable instructions, but the directions are as different as English and Chinese.
So can't you teach Linux how to understand directions intended for DOS, and run the same file on both? Yes you can, and that's what DosEmu did back in the day. It's also how Linux+Wine runs Windows software, and how FreeBSD runs Linux software. However, it's a lot of headache and additional work, and may still not be very compatible.
回答7:
I post this reply to Andrey's discussion about ABIs as an answer because it is too much for a comment and requires the formatting of an Answer.
Andrey, what you are showing has nothing to do with Linux or Windows. It is an example of a development environment using certain conventions. All object modules and modules in libraries must adhere to these conventions, and nothing else. It isn't Linux or Windows that expect values in certain registers, it is the development environment.
The following is the more standard way of C calling conventions (Visual Stdio 2008). In all cases, the caller must evaluate parameters right-to-left as per the C standard:
int f(int a, int b, int (*g)(int, int))
{
push ebp
mov ebp,esp
return g(a * 2, b * 3) * 4;
mov eax,dword ptr [ebp+0Ch]
imul eax,eax,3
push eax
mov ecx,dword ptr [ebp+8]
shl ecx,1
push ecx
call dword ptr [ebp+10h]
add esp,8
shl eax,2
mov esp,ebp
pop ebp
ret
}
The caller pushes the parameters right-to-left and calls the callee
the callee saves the stack frame pointer, usually
ebp
on Intel, and adds toesp
for local storage (none here)The callee references the parameters relative to
ebp
The callee performs its function
The callee restores
ebp
and returnsThe caller removes the parameters of the call from the stack, e.g.
add esp,8
Again, it is the development environment that dictates these conventions, not the OS. The OS may have its own conventions for applications to request services. These are then implemented in the OS-targeted libraries.
来源:https://stackoverflow.com/questions/30574728/why-we-must-recompile-a-c-source-code-for-a-different-os-on-the-same-machine