Discussion:
Making a Fat Binary for Linux and Mac
(too old to reply)
Frederick Gotham
2020-10-22 10:48:37 UTC
Permalink
I have an idea for making a 'fat binary' that doesn't require any kernel
support on Linux or Mac.

So let's say we start off with a small shell script like this:

#!/bin/sh

arch=`uname -m`

if [ $arch = "x86_64" ] ; then
// Do something
elif [ $arch = "aarch64" ] ; then
// Do something
fi

exit 0

The most important line is the last one "exit 0" because it will stop
the script. It is after this line that I will put the x86_64 binary,
followed by the aarch64 binary, so the script will look something like
this:

#!/bin/sh

arch=`uname -m`

if [ $arch = "x86_64" ] ; then
skip=00290
count=08816
elif [ $arch = "aarch64" ] ; then
skip=09106
count=14688
else
exit 1
fi

dd if="$0" of=/tmp/binaryfile bs=1 skip=${skip} count=${count} > /dev/null 2>&1

chmod +x /tmp/binaryfile
/tmp/binaryfile

exit 0
[ - - - x86_64 binary goes here - - - ]
[ - - - aarch64 binary goes here - - - ]

So as a test program to try it out, I'll now write a small C++ program
that prints a number sequence:

#include <iostream> /* cout, endl */
#include <thread> /* this_thread::sleep_for */
#include <chrono> /* seconds */

auto main(void) -> int
{
for ( unsigned i = 0; /* ever */ ; ++i )
{
std::this_thread::sleep_for(std::chrono::seconds(1u));
std::cout << i << std::endl;
}
}

I have compiled this program for two architectures:

g++ main.cpp -DNDEBUG -Os -o prog_x86_64
aarch64-linux-gnu-g++ main.cpp -DNDEBUG -Os -o prog_aarch64

and so I have two binaries as follows:

-rwxr-xr-x 1 root root 8816 Oct 22 10:53 prog_x86_64
-rwxr-xr-x 1 root root 14688 Oct 22 10:52 prog_aarch64

The x86_64 binary is 8816 bytes.
The aarch64 binary is 14688 bytes.

So now I get the size of my small script above:

-rwxr-xr-x 1 root root 290 Oct 22 11:31 myscript.sh

It is 290 bytes, which means I know how much to skip by:

dd if="$0" of=/tmp/binaryfile bs=1 skip=00290 count=08816

Finally I concatenate the 3 files:

cat myscript.sh prog_x86_64 prog_aarch64 > fatbin

And I run my fat binary:

chmod +x fatbin
./fatbin

I've tried this 'fatbin' file on both architectures and it works. You
can download the files here and try it yourself:

http://virjacode.com/experimental/prog.cpp
http://virjacode.com/experimental/prog_aarch64
http://virjacode.com/experimental/prog_x86_64
http://virjacode.com/experimental/myscript.sh
http://virjacode.com/experimental/fatbin

I think it might even be possible to extend this script so that it
doubles as a batch file for Microsft Windows, so maybe we could have two
Linux binaries, a Mac binary, as well as a Microsoft binary all in one
file. It can made threadsafe and re-enterable by using system-supplied
temporary filenames or UUID's instead of "/tmp/binaryfile".
Leo
2020-10-22 13:19:20 UTC
Permalink
Wow, this is a really good approach. I'm sure it can be automated to
calculate to offsets automatically as well.

I think it would probably work on stuff like WSL and Cygwin. You can
even do fancier stuff like checking OS instead of just the architecture.
Frederick Gotham
2020-10-22 13:42:10 UTC
Permalink
Post by Leo
Wow, this is a really good approach. I'm sure it can be automated to
calculate to offsets automatically as well.
I think it would probably work on stuff like WSL and Cygwin. You can
even do fancier stuff like checking OS instead of just the architecture.
I've already written the program to compile it for 4 architectures and calculate the offsets.

First we start off with this script:

#!/bin/sh

# x86 32-Bit = i386, i686
# x86 64-bit = x86_64
# arm 32-Bit = arm
# arm 64-Bit = aarch64 aarch64_be armv8b armv8l

arch=`uname -m`

case ${arch} in
i386|i686)
skip=AAAAskipAAAA
count=AAAAcountAAA
;;
x86_64)
skip=BBBBskipBBBB
count=BBBBcountBBB
;;
arm|armv7l)
skip=CCCCskipCCCC
count=CCCCcountCCC
;;
aarch64|aarch64_be|armv8b|armv8l)
skip=DDDDskipDDDD
count=DDDDcountDDD
;;
*)
exit 1
;;
esac

dd if="$0" of=/tmp/binaryfile bs=1 skip=${skip} count=${count} > /dev/null 2>&1

chmod +x /tmp/binaryfile
/tmp/binaryfile

exit 0


And next we have the C++ program that makes the 4 binaries along with "generated_script.sh":


#include <cstdlib> /* system */
#include <iostream>
#include <string> /* string, to_string */
#include <fstream> /* ifstream::pos_type */
#include <algorithm> /* replace */

inline std::ifstream::pos_type FileSize(char const *const filename)
{
std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
return in.tellg();
}

using std::cout;
using std::endl;
using std::string;

char const *const g_archs[] =
{ "x86_32", "x86_64", "arm32", "arm64" };

#define N_ARCHES (sizeof g_archs / sizeof *g_archs)

char const *const g_compiler_commands[] =
{
"g++ -m32",
"g++",
"arm-linux-gnueabihf-g++",
"aarch64-linux-gnu-g++"
};

char const g_universal_compiler_instructions[] = " -Os -DNDEBUG ./main.cpp";

size_t g_binary_sizes[N_ARCHES];

void Print(void);

auto main(void) -> int
{
for ( unsigned i = 0; i != N_ARCHES; ++i )
{
string const cmd = string(g_compiler_commands[i]) + " -o prog_" + g_archs[i] + g_universal_compiler_instructions;

cout << "Running compiler command: " << cmd << endl;

std::system( cmd.c_str() );

g_binary_sizes[i] = FileSize( (string("prog_") + g_archs[i]).c_str() );

cout << "File Size: " << g_binary_sizes[i] << " bytes" << endl;
}

Print();
}

template<typename T/*, typename = std::enable_if_t<std::is_integral_v<T>>*/>
std::string to_string_with_zero_padding(const T& value, std::size_t total_length)
{
auto str = std::to_string(value);
if (str.length() < total_length)
str.insert(str.front() == '-' ? 1 : 0, total_length - str.length(), '0');
return str;
}

void Print(void)
{
std::system("cp ./my_script.sh ./generated_script.sh");

size_t skipsum = FileSize("./generated_script.sh");

string str_skip ( "AAAAskipAAAA" ),
str_count( "AAAAcountAAA" );

char counterchar = 'A';

for ( unsigned i = 0; i != N_ARCHES; ++i, ++counterchar )
{
std::system( ("sed -i 's/" + str_skip + "/" + to_string_with_zero_padding(skipsum,12) + "/g' ./generated_script.sh").c_str() );
std::system( ("sed -i 's/" + str_count + "/" + to_string_with_zero_padding(g_binary_sizes[i],12) + "/g' ./generated_script.sh").c_str() );

std::replace( str_skip.begin(), str_skip.end(), counterchar, static_cast<char>(counterchar + 1u)); // replace all 'x' to 'y'
std::replace( str_count.begin(), str_count.end(), counterchar, static_cast<char>(counterchar + 1u)); // replace all 'x' to 'y'

skipsum += g_binary_sizes[i];
}
}


I've tested all this on 3 architectures (arm32, arm64, x86_64) and it works.
Frederick Gotham
2020-10-22 14:48:06 UTC
Permalink
You can even do fancier stuff like checking
OS instead of just the architecture.
I can get the Unix-like operating systems as follows:

OS="`uname`"
case ${OS} in
'Linux')
OS='Linux'
;;
'FreeBSD')
OS='FreeBSD'
;;
'WindowsNT')
OS='Windows'
;;
'Darwin')
OS='Mac'
;;
'SunOS')
OS='Solaris'
;;
'AIX') ;;
*) ;;
esac

The real trick will be to write a Linux shell script that will also run as an MS-DOS batch file, and to get the fat binary to run seamlessly across Linux, Mac and MS-Windows.
Frederick Gotham
2020-10-22 14:51:17 UTC
Permalink
Post by Frederick Gotham
The real trick will be to write a Linux shell script
that will also run as an MS-DOS batch file, and to
get the fat binary to run seamlessly across Linux,
Mac and MS-Windows.
Actually I found this just now, it's not very complicated

: # This is a special script which intermixes both sh
: # and cmd code. It is written this way because it is
: # used in system() shell-outs directly in otherwise
: # portable code. See https://stackoverflow.com/questions/17510688
: # for details.
:; echo "This is ${SHELL}"; exit
@ECHO OFF
ECHO This is %COMSPEC%

So the next trick will be to get the batch file part of it to do something similar to what I did was "dd" in Linux.
Frederick Gotham
2020-10-22 15:19:05 UTC
Permalink
Post by Frederick Gotham
Post by Frederick Gotham
The real trick will be to write a Linux shell script
that will also run as an MS-DOS batch file, and to
get the fat binary to run seamlessly across Linux,
Mac and MS-Windows.
Actually I found this just now, it's not very complicated
: # This is a special script which intermixes both sh
: # and cmd code. It is written this way because it is
: # used in system() shell-outs directly in otherwise
: # portable code. See https://stackoverflow.com/questions/17510688
: # for details.
:; echo "This is ${SHELL}"; exit
@ECHO OFF
ECHO This is %COMSPEC%
So the next trick will be to get the batch file part of it to do something similar to what I did was "dd" in Linux.
Just found a way of embedding a binary in a batch file, so in the next day or two I might have a fat binary that runs on MS-Windows and also on 4 architectures of Linux.

I've copy-pasted the following from StackOverflow:

You could simply append the binary part to your batch file with 'copy', as follows:

copy /a batchBin.bat + /b myBinaryFile.bin /b combined.bat

batchBin.bat (The last line with exit /b should end with a newline)

;;;===,,,@echo off
;;;===,,,echo line2
;;;===,,,findstr /v "^;;;===,,," "%~f0" > output.bin
;;;===,,,exit /b

The key is the findstr command, it outputs all lines not beginning with ;;;===,,,.
And as each of them are standard batch delimiters, they can be prefix any command in a batch file in any combination.
Ben Bacarisse
2020-10-22 13:52:42 UTC
Permalink
Post by Frederick Gotham
I have an idea for making a 'fat binary' that doesn't require any kernel
support on Linux or Mac.
This is not on-topic here (but I seem to remember you don't care about
that).
Post by Frederick Gotham
#!/bin/sh
arch=`uname -m`
if [ $arch = "x86_64" ] ; then
// Do something
elif [ $arch = "aarch64" ] ; then
// Do something
fi
exit 0
The most important line is the last one "exit 0" because it will stop
the script. It is after this line that I will put the x86_64 binary,
followed by the aarch64 binary, so the script will look something like
#!/bin/sh
arch=`uname -m`
if [ $arch = "x86_64" ] ; then
skip=00290
count=08816
elif [ $arch = "aarch64" ] ; then
skip=09106
count=14688
else
exit 1
fi
dd if="$0" of=/tmp/binaryfile bs=1 skip=${skip} count=${count} > /dev/null 2>&1
chmod +x /tmp/binaryfile
/tmp/binaryfile
exit 0
[ - - - x86_64 binary goes here - - - ]
[ - - - aarch64 binary goes here - - - ]
I would use a single archive file so there is no need to know sizes.
And I'd exec the file rather than use exit 0.
--
Ben.
Arnold Ziffel
2020-10-22 14:05:00 UTC
Permalink
Post by Frederick Gotham
I have an idea for making a 'fat binary' that doesn't require any kernel
support on Linux or Mac.
Not that it's on-topic here, but some time ago I wrote a program that can
be launched both as .COM file and as .BAT file.

; Compile with:
; nasm16 -f bin -o combat.com combat.asm
; copy combat.com combat.bat

org 100h

start:
; Two colons so .BAT treats it as a label
cmp bh, [bp+si]

; Replace "BAT " with "COM "
mov di, tx_type
mov dword [di], 'COM '

; Add end-of-string marker for DOS
mov di, tx_end
mov byte [di], '$'

; Print text
mov ah, 9
mov dx, tx_text
int 21h

; Termination
ret

; CRLF (to end the BAT label)
mov ax, 0A0Dh

; AT-sign -- to hide the command (for BAT)
inc ax

; "EC"
inc bp
inc bx

; "ho "
push word 206Fh

tx_text:
db "I'm running as "
tx_type:
db "BAT :)", 0Dh, 0Ah
tx_end:
--
One sees more clearly backward than forward.
Scott Lurndal
2020-10-22 15:25:33 UTC
Permalink
Post by Frederick Gotham
I have an idea for making a 'fat binary' that doesn't require any kernel
support on Linux or Mac.
#!/bin/sh
arch=`uname -m`
if [ $arch = "x86_64" ] ; then
// Do something
elif [ $arch = "aarch64" ] ; then
// Do something
fi
exit 0
https://en.wikipedia.org/wiki/Shar

Loading...