diff --git a/erlang/Makefile b/erlang/Makefile
index 9262778..d827816 100644
--- a/erlang/Makefile
+++ b/erlang/Makefile
@@ -21,7 +21,7 @@ REBAR3=$(shell type -p rebar3 || echo ./rebar3)
REBAR3_GIT=https://github.com/erlang/rebar3.git
REBAR3_VSN=3.7.5
-PROGRAMS=8to9 ar as nm od readelf
+PROGRAMS=8to9 ar as ld nm od readelf
default: compile link
diff --git a/erlang/apps/ld/src/ld.app.src b/erlang/apps/ld/src/ld.app.src
new file mode 100644
index 0000000..817acbd
--- /dev/null
+++ b/erlang/apps/ld/src/ld.app.src
@@ -0,0 +1,25 @@
+%%% Copyright (C) 2020 Mikael Pettersson
+%%%
+%%% This file is part of pdp10-tools.
+%%%
+%%% pdp10-tools is free software: you can redistribute it and/or modify
+%%% it under the terms of the GNU General Public License as published by
+%%% the Free Software Foundation, either version 3 of the License, or
+%%% (at your option) any later version.
+%%%
+%%% pdp10-tools is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+%%% GNU General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with pdp10-tools. If not, see .
+
+{application, ld,
+ [{description, "'ld' clone for pdp10-elf"},
+ {vsn, "0.1.0"},
+ {registered, []},
+ {applications, [kernel, stdlib, lib]},
+ {env, []},
+ {modules, []}
+ ]}.
diff --git a/erlang/apps/ld/src/ld.erl b/erlang/apps/ld/src/ld.erl
new file mode 100644
index 0000000..fc23505
--- /dev/null
+++ b/erlang/apps/ld/src/ld.erl
@@ -0,0 +1,384 @@
+%%% -*- erlang-indent-level: 2 -*-
+%%%
+%%% 'ld' clone for pdp10-elf
+%%% Copyright (C) 2020 Mikael Pettersson
+%%%
+%%% This file is part of pdp10-tools.
+%%%
+%%% pdp10-tools is free software: you can redistribute it and/or modify
+%%% it under the terms of the GNU General Public License as published by
+%%% the Free Software Foundation, either version 3 of the License, or
+%%% (at your option) any later version.
+%%%
+%%% pdp10-tools is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+%%% GNU General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with pdp10-tools. If not, see .
+
+-module(ld).
+-export([ main/1
+ , format_error/1
+ ]).
+
+-define(EMULATION, "elf36_pdp10").
+-define(OBJFORMAT, "elf36-pdp10").
+
+-record(options,
+ { entry = "_start" :: string()
+ , files = [] :: [{file, string()} | {library, string()}]
+ , fini = "_fini" :: string()
+ , init = "_init" :: string()
+ , library_path = [] :: [string()]
+ , output = "a.out" :: string()
+ , page_align_sections = false :: boolean()
+ , rwtext = false :: boolean()
+ , section_starts = [] :: [{string(), integer()}]
+ , trace = 0 :: non_neg_integer()
+ }).
+
+%% Command-line interface ======================================================
+
+main(Argv) ->
+ escript_runtime:start(fun main_/1, Argv).
+
+-spec main_([string()]) -> no_return().
+main_(Argv) ->
+ case ld(Argv) of
+ ok -> halt(0);
+ {error, Reason} ->
+ escript_runtime:fatal("~s\n", [error:format(Reason)])
+ end.
+
+ld(Argv) ->
+ %% NYI options:
+ %% @file
+ %% -a
+ %% --audit
+ %% -c / --mri-script
+ %% -d / -dc / -dp
+ %% -P / --depaudit
+ %% --exclude-libs
+ %% --exclude-modules-for-imlib
+ %% -E / --[no-]export-dynamic
+ %% -EL
+ %% -f / --auxiliary
+ %% -F / --filter
+ %% -g
+ %% -G / --gpsize
+ %% -h / -soname
+ %% -M / --print-map
+ %% --[no-]print-map-discarded
+ %% -O
+ %% -plugin
+ %% --push-state / --pop-state
+ %% -q / --emit-relocs
+ %% --force-dynamic
+ %% -r / --relocatable / -i
+ %% -R / --just-symbols
+ %% -s / --strip-all
+ %% -S / --strip-debug
+ %% --[no-]strip-discarded
+ %% -T / --script
+ %% -dT / --default-script
+ %% -u / --undefined
+ %% --require-defined
+ %% -Ur
+ %% --orphan-handling
+ %% --unique
+ %% -x / --discard-all
+ %% -X / --discard-locals
+ %% -y / --trace-symbol
+ %% -Y
+ %% -z
+ %% -( / -) / --start-group / --end-group
+ %% --[no-]accept-unknown-input-arch
+ %% --[no-]as-needed
+ %% -assert
+ %% -Bdynamic / -dy / -call_shared
+ %% -Bgroup
+ %% -Bstatic / -dn / -non_shared / -static
+ %% -Bsymbolic
+ %% -Bsymbolic-functions
+ %% --dynamic-list
+ %% --dynamic-list-data
+ %% --dynamic-list-cpp-new
+ %% --dynamic-list-cpp-typeinfo
+ %% --[no-]check-sections
+ %% --[no-]copy-dt-needed-entries / --[no-]add-needed
+ %% --cref
+ %% --no-define-common
+ %% --force-group-allocation
+ %% --defsym
+ %% --[no-]demangle
+ %% -I / --dynamic-linker
+ %% --no-dynamic-linker
+ %% --embedded-relocs
+ %% --disable-multiple-abs-defs
+ %% --[no-]fatal-warnings
+ %% --force-exe-suffix
+ %% --[no-]gc-sections
+ %% --[no-]print-gc-sections
+ %% --gc-keep-exported
+ %% --print-memory-usage
+ %% -Map
+ %% --no-keep-memory
+ %% --no-undefined / -z defs
+ %% --allow-multiple-definitions / -z muldefs
+ %% --[no-]allow-shlib-undefined
+ %% --no-undefined-version
+ %% --default-symver
+ %% --default-imported-symver
+ %% --no-warn-mismatch
+ %% --no-warn-search-mismatch
+ %% --noinhibit-exec
+ %% -nostdlib
+ %% --out-implib
+ %% -pie / --pic-executable
+ %% -qmagic
+ %% -Qy
+ %% --[no-]relax
+ %% --retain-symbols-file
+ %% -rpath
+ %% -rpath-link
+ %% -shared / -Bshareable
+ %% --sort-common
+ %% --sort-section
+ %% --spare-dynamic-tags
+ %% --split-by-file
+ %% --split-by-reloc
+ %% --stats
+ %% --sysroot
+ %% --task-link
+ %% --traditional-format
+ %% -Tldata-segment
+ %% --unresolved-symbols
+ %% --dll-verbose / --verbose
+ %% --version-script
+ %% --warn-common
+ %% --warn-constructors
+ %% --warn-multiple-gp
+ %% --warn-once
+ %% --warn-section-align
+ %% --warn-shared-textrel
+ %% --warn-alternate-em
+ %% --warn-unresolved-symbols / --error-unresolved-symbols
+ %% --[no-]whole-archive
+ %% --wrap
+ %% --[no-]eh-frame-hdr
+ %% --no-ld-generated-unwind-info
+ %% --enable-new-dtags / --disable-new-dtags
+ %% --hash-size
+ %% --hash-style
+ %% --compress-debug-sections
+ %% --reduce-memory-overheads
+ %% --build-id
+ %% All other target-specific options.
+ %%
+ %% LD has quite archaic option syntax:
+ %% - it takes some single-dash long options, e.g. -nostdlib
+ %% - options and non-options must be processed in the order given,
+ %% e.g. due to --start-group ... --end-group
+ case getopt:parse(Argv, "-b:e:l:L:m:nNo:tvV",
+ [ %% single-dash long options
+ { "-EB", no, 'EB' }
+ , { "-fini", required, fini }
+ , { "-init", required, init }
+ , { "-Tbss", required, 'Tbss' }
+ , { "-Tdata", required, 'Tdata' }
+ , { "-Trodata-segment", required, 'Trodata' }
+ , { "-Ttext", required, 'Ttext' }
+ , { "-Ttext-segment", required, 'Ttext' }
+ %% long-only options
+ , { "help", no, help }
+ , { "no-omagic", no, no_omagic }
+ , { "oformat", required, oformat }
+ , { "print-output-format", no, print_output_format }
+ , { "section-start", required, section_start }
+ , { "target-help", no, target_help }
+ %% long aliases for short options
+ , { "entry", required, $e }
+ , { "format", required, $b }
+ , { "library", required, $l }
+ , { "library-path", required, $L }
+ , { "nmagic", no, $n }
+ , { "omagic", no, $N }
+ , { "output", required, $o }
+ , { "trace", no, $t }
+ , { "version", no, $v }
+ ]) of
+ {ok, {Opts, _NonOpts = []}} ->
+ case process_options(Opts) of
+ {ok, Options} ->
+ %% FIXME: input(Options) -> {ok, _} | {error, _}
+ {ok, TUnit} = input(Options),
+ output(Options, TUnit);
+ {error, _Reason} = Error -> Error
+ end;
+ {error, _Reason} = Error -> Error
+ end.
+
+process_options(Opts) -> process_options(Opts, #options{}).
+
+process_options([Opt | Opts], Options) ->
+ case process_option(Opt, Options) of
+ {ok, NewOptions} -> process_options(Opts, NewOptions);
+ {error, _Reason} = Error -> Error
+ end;
+process_options([], Options) ->
+ #options{ files = Files, library_path = LibraryPath } = Options,
+ {ok, Options#options{ files = lists:reverse(Files)
+ , library_path = lists:reverse(LibraryPath)
+ }}.
+
+process_option(Opt, Options) ->
+ case Opt of
+ 'EB' -> handle_EB(Opt, Options);
+ {$m, _} -> handle_emulation(Opt, Options);
+ {$e, _} -> handle_entry(Opt, Options);
+ {fini, _} -> handle_fini(Opt, Options);
+ {$b, _} -> handle_format(Opt, Options);
+ help -> handle_help(Opt, Options);
+ {init, _} -> handle_init(Opt, Options);
+ {$l, _} -> handle_library(Opt, Options);
+ {$L, _} -> handle_library_path(Opt, Options);
+ $n -> handle_nmagic(Opt, Options);
+ no_omagic -> handle_no_omagic(Opt, Options);
+ {oformat, _} -> handle_oformat(Opt, Options);
+ $N -> handle_omagic(Opt, Options);
+ {$o, _} -> handle_output(Opt, Options);
+ print_output_format -> handle_print_output_format(Opt, Options);
+ {section_start, _} -> handle_section_start(Opt, Options);
+ target_help -> handle_target_help(Opt, Options);
+ {'Tbss', _} -> handle_Tbss(Opt, Options);
+ {'Tdata', _} -> handle_Tdata(Opt, Options);
+ $t -> handle_trace(Opt, Options);
+ {'Trodata', _} -> handle_Trodata(Opt, Options);
+ {'Ttext', _} -> handle_Ttext(Opt, Options);
+ $V -> handle_V(Opt, Options);
+ $v -> handle_version(Opt, Options)
+ end.
+
+%% Option Handlers =============================================================
+
+handle_EB('EB', Options) ->
+ {ok, Options}.
+
+handle_emulation({$m, Emulation}, Options) ->
+ case Emulation of
+ ?EMULATION -> {ok, Options};
+ _ -> {error, {?MODULE, {invalid, emulation, Emulation}}}
+ end.
+
+handle_entry({$e, Entry}, Options) ->
+ {ok, Options#options{entry = Entry}}.
+
+handle_fini({fini, Fini}, Options) ->
+ {ok, Options#options{fini = Fini}}.
+
+handle_format({$b, InputFormat}, Options) ->
+ case InputFormat of
+ ?OBJFORMAT -> {ok, Options};
+ _ -> {error, {?MODULE, {invalid, "input format", InputFormat}}}
+ end.
+
+handle_help(help, _Options) ->
+ io:format("--help: NYI\n"),
+ exit(0).
+
+handle_init({init, Init}, Options) ->
+ {ok, Options#options{init = Init}}.
+
+handle_library({$l, Library}, Options) ->
+ {ok, Options#options{files = [{library, Library} | Options#options.files]}}.
+
+handle_library_path({$L, Path}, Options) ->
+ {ok, Options#options{library_path = [Path | Options#options.library_path]}}.
+
+handle_nmagic($n, Options) ->
+ {ok, Options#options{page_align_sections = false}}.
+
+handle_no_omagic(no_omagic, Options) ->
+ {ok, Options#options{rwtext = false, page_align_sections = true}}.
+
+handle_oformat({oformat, OutputFormat}, Options) ->
+ case OutputFormat of
+ ?OBJFORMAT -> {ok, Options};
+ _ -> {error, {?MODULE, {invalid, "output format", OutputFormat}}}
+ end.
+
+handle_omagic($N, Options) ->
+ {ok, Options#options{rwtext = true, page_align_sections = false}}.
+
+handle_output({$o, Output}, Options) ->
+ {ok, Options#options{output = Output}}.
+
+handle_print_output_format(print_output_format, Options) ->
+ io:format(?OBJFORMAT "\n"),
+ {ok, Options}.
+
+handle_section_start({section_start, Arg}, Options) ->
+ case string:split(Arg, "=") of
+ [Name, Org] -> section_start(Name, Org, Options);
+ _ -> {error, {?MODULE, {invalid, "section-start", Arg}}}
+ end.
+
+handle_target_help(target_help, _Options) ->
+ io:format("No target-specific options apply.\n"),
+ exit(0).
+
+handle_Tbss({'Tbss', Org}, Options) ->
+ section_start(".bss", Org, Options).
+
+handle_Tdata({'Tdata', Org}, Options) ->
+ section_start(".data", Org, Options).
+
+handle_trace($t, Options) ->
+ {ok, Options#options{trace = Options#options.trace + 1}}.
+
+handle_Trodata({'Trodata', Org}, Options) ->
+ section_start(".rodata", Org, Options).
+
+handle_Ttext({'Ttext', Org}, Options) ->
+ section_start(".text", Org, Options).
+
+handle_V($V, Options) ->
+ version(),
+ io:format(" Supported emulations:\n"),
+ io:format(" " ?EMULATION "\n"),
+ {ok, Options}.
+
+handle_version($v, Options) ->
+ version(),
+ {ok, Options}.
+
+section_start(Name, Org, Options) ->
+ case strtol:parse(Org, _Base = 16) of
+ {ok, {Value, _Rest = []}} ->
+ SectionStarts = Options#options.section_starts,
+ NewSectionStarts = lists:keystore(Name, 1, SectionStarts, {Name, Value}),
+ {ok, Options#options{section_starts = NewSectionStarts}};
+ _ -> {error, {?MODULE, {invalid, "org for section " ++ Name, Org}}}
+ end.
+
+version() ->
+ io:format("pdp10-tools ld version 0.1\n").
+
+%% Input Processing ============================================================
+
+input(_Options) -> {ok, []}. % FIXME
+
+%% Output Generation ===========================================================
+
+output(_Options, []) -> ok. % FIXME
+
+%% Error Formatting ============================================================
+
+-spec format_error(term()) -> io_lib:chars().
+format_error(Reason) ->
+ case Reason of
+ {invalid, What, Parameter} ->
+ io_lib:format("invalid ~s: ~s", [What, Parameter])
+ end.
diff --git a/erlang/rebar.config b/erlang/rebar.config
index 977449b..eb4c84d 100644
--- a/erlang/rebar.config
+++ b/erlang/rebar.config
@@ -44,6 +44,7 @@
{'8to9', main, 1}
, {ar, main, 1}
, {as, main, 1}
+ , {ld, main, 1}
, {nm, main, 1}
, {od, main, 1}
, {readelf, main, 1}
@@ -51,6 +52,7 @@
, {assemble, format_error, 1}
, {getopt, format_error, 1}
, {input, format_error, 1}
+ , {ld, format_error, 1}
, {output, format_error, 1}
, {parse, format_error, 1}
, {pdp10_elf36, format_error, 1}
@@ -69,6 +71,7 @@
[ {'8to9', [{escript_main_app, '8to9'}]}
, {ar, [{escript_main_app, ar}]}
, {as, [{escript_main_app, as}]}
+ , {ld, [{escript_main_app, ld}]}
, {nm, [{escript_main_app, nm}]}
, {od, [{escript_main_app, od}]}
, {readelf, [{escript_main_app, readelf}]}