diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java
index dfe21978f..dca54484d 100644
--- a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java
+++ b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java
@@ -31,6 +31,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.function.Supplier;
 import org.teavm.ast.InvocationExpr;
 import org.teavm.ast.decompilation.Decompiler;
 import org.teavm.backend.lowlevel.analyze.LowLevelInliningFilterFactory;
@@ -39,6 +40,7 @@ import org.teavm.backend.lowlevel.generate.NameProvider;
 import org.teavm.backend.lowlevel.generate.NameProviderWithSpecialNames;
 import org.teavm.backend.lowlevel.transform.CoroutineTransformation;
 import org.teavm.backend.wasm.binary.BinaryWriter;
+import org.teavm.backend.wasm.generate.DwarfGenerator;
 import org.teavm.backend.wasm.generate.WasmClassGenerator;
 import org.teavm.backend.wasm.generate.WasmDependencyListener;
 import org.teavm.backend.wasm.generate.WasmGenerationContext;
@@ -76,6 +78,7 @@ import org.teavm.backend.wasm.intrinsics.WasmIntrinsicFactory;
 import org.teavm.backend.wasm.intrinsics.WasmIntrinsicFactoryContext;
 import org.teavm.backend.wasm.intrinsics.WasmIntrinsicManager;
 import org.teavm.backend.wasm.intrinsics.WasmRuntimeIntrinsic;
+import org.teavm.backend.wasm.model.WasmCustomSection;
 import org.teavm.backend.wasm.model.WasmFunction;
 import org.teavm.backend.wasm.model.WasmLocal;
 import org.teavm.backend.wasm.model.WasmMemorySegment;
@@ -491,6 +494,10 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
                 classGenerator, stringPool, obfuscated);
         context.addIntrinsic(exceptionHandlingIntrinsic);
 
+        var dwarfGenerator = debugging ? new DwarfGenerator() : null;
+        if (dwarfGenerator != null) {
+            dwarfGenerator.begin();
+        }
         var generator = new WasmGenerator(decompiler, classes, context, classGenerator, binaryWriter,
                 asyncMethods::contains);
 
@@ -539,7 +546,7 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
 
         var writer = new WasmBinaryWriter();
         var renderer = new WasmBinaryRenderer(writer, version, obfuscated);
-        renderer.render(module);
+        renderer.render(module, buildDwarf(dwarfGenerator));
 
         try (OutputStream output = buildTarget.createResource(outputName)) {
             output.write(writer.getData());
@@ -558,6 +565,16 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
         }
     }
 
+    private Supplier<Collection<? extends WasmCustomSection>> buildDwarf(DwarfGenerator generator) {
+        if (generator == null) {
+            return null;
+        }
+        return () -> {
+            generator.end();
+            return generator.createSections();
+        };
+    }
+
     private WasmFunction createStartFunction(NameProvider names) {
         var function = new WasmFunction("teavm_start");
         function.setExportName("start");
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfAbbreviation.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfAbbreviation.java
new file mode 100644
index 000000000..ce1c1a917
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfAbbreviation.java
@@ -0,0 +1,33 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf;
+
+import java.util.function.Consumer;
+import org.teavm.backend.wasm.dwarf.blob.Blob;
+
+public class DwarfAbbreviation {
+    int tag;
+    boolean hasChildren;
+    Consumer<Blob> writer;
+    int index;
+    int count;
+
+    DwarfAbbreviation(int tag, boolean hasChildren, Consumer<Blob> writer) {
+        this.tag = tag;
+        this.hasChildren = hasChildren;
+        this.writer = writer;
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfConstants.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfConstants.java
new file mode 100644
index 000000000..a9b006aa4
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfConstants.java
@@ -0,0 +1,30 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf;
+
+public final class DwarfConstants {
+    public static final int DWARF_VERSION = 5;
+
+    public static final int DW_UT_COMPILE = 0x01;
+
+    public static final int DW_TAG_COMPILE_UNIT = 0x11;
+
+    public static final int DW_CHILDREN_YES = 1;
+    public static final int DW_CHILDREN_NO = 0;
+
+    private DwarfConstants() {
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfInfoWriter.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfInfoWriter.java
new file mode 100644
index 000000000..61038cdd2
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfInfoWriter.java
@@ -0,0 +1,174 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf;
+
+import static org.teavm.backend.wasm.dwarf.DwarfConstants.DW_CHILDREN_NO;
+import static org.teavm.backend.wasm.dwarf.DwarfConstants.DW_CHILDREN_YES;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Consumer;
+import org.teavm.backend.wasm.dwarf.blob.BinaryDataConsumer;
+import org.teavm.backend.wasm.dwarf.blob.Blob;
+import org.teavm.backend.wasm.dwarf.blob.Marker;
+
+public class DwarfInfoWriter {
+    private Blob output = new Blob();
+    private List<DwarfAbbreviation> abbreviations = new ArrayList<>();
+    private List<Placement> placements = new ArrayList<>();
+    private Marker placeholderMarker;
+
+    public DwarfInfoWriter write(byte[] data) {
+        output.write(data);
+        return this;
+    }
+
+    public DwarfInfoWriter write(byte[] data, int offset, int limit) {
+        output.write(data, offset, limit);
+        return this;
+    }
+
+    public DwarfInfoWriter writeInt(int value) {
+        output.writeInt(value);
+        return this;
+    }
+
+    public DwarfInfoWriter writeShort(int value) {
+        output.writeShort(value);
+        return this;
+    }
+
+    public DwarfInfoWriter writeByte(int value) {
+        output.write((byte) value);
+        return this;
+    }
+
+    public DwarfInfoWriter writeLEB(int value) {
+        output.writeLEB(value);
+        return this;
+    }
+
+    public DwarfAbbreviation abbreviation(int tag, boolean hasChildren, Consumer<Blob> blob) {
+        var abbr = new DwarfAbbreviation(tag, hasChildren, blob);
+        abbreviations.add(abbr);
+        return abbr;
+    }
+
+    public DwarfInfoWriter tag(DwarfAbbreviation abbreviation) {
+        placements.add(new Placement(output.ptr()) {
+            @Override
+            void write(Blob blob) {
+                blob.writeLEB(abbreviation.index);
+            }
+        });
+        abbreviation.count++;
+        return this;
+    }
+
+    public DwarfInfoWriter emptyTag() {
+        output.write((byte) 0);
+        return this;
+    }
+
+    public DwarfPlaceholder placeholder(int size) {
+        return new DwarfPlaceholder(size);
+    }
+
+    public DwarfInfoWriter ref(DwarfPlaceholder placeholder, DwarfPlaceholderWriter writer) {
+        placements.add(new Placement(output.ptr()) {
+            @Override
+            void write(Blob blob) {
+                if (placeholder.ptr >= 0) {
+                    placeholderMarker.update();
+                    writer.write(blob, placeholder.ptr);
+                    placeholderMarker.rewind();
+                    blob.skip(placeholder.size);
+                } else {
+                    placeholder.addForwardRef(writer, blob.marker());
+                    blob.skip(placeholder.size);
+                }
+            }
+        });
+        return this;
+    }
+
+    public DwarfInfoWriter mark(DwarfPlaceholder placeholder) {
+        placements.add(new Placement(output.ptr()) {
+            @Override
+            void write(Blob blob) {
+                if (placeholder.ptr >= 0) {
+                    throw new IllegalStateException();
+                }
+                placeholder.ptr = blob.ptr();
+                if (placeholder.forwardReferences != null) {
+                    placeholderMarker.update();
+                    for (var forwardRef : placeholder.forwardReferences) {
+                        forwardRef.marker.rewind();
+                        forwardRef.writer.write(blob, placeholder.ptr);
+                    }
+                    placeholder.forwardReferences = null;
+                    placeholderMarker.rewind();
+                }
+            }
+        });
+        return this;
+    }
+
+    public void buildAbbreviations(Blob target) {
+        var orderedAbbreviations = new ArrayList<>(abbreviations);
+        orderedAbbreviations.sort(Comparator.comparingInt(a -> -a.count));
+        var sz = orderedAbbreviations.size();
+        while (sz > 0 && orderedAbbreviations.get(sz - 1).count == 0) {
+            --sz;
+        }
+        for (var i = 0; i < sz; ++i) {
+            var abbrev = orderedAbbreviations.get(i);
+            abbrev.index = i + 1;
+            target.writeLEB(abbrev.index).writeLEB(abbrev.tag)
+                    .writeByte(abbrev.hasChildren ? DW_CHILDREN_YES : DW_CHILDREN_NO);
+            abbrev.writer.accept(target);
+            target.writeByte(0).writeByte(0);
+        }
+        target.writeByte(0);
+    }
+
+    public void build(Blob target) {
+        placeholderMarker = target.marker();
+        this.targetBlob = target;
+        var reader = output.newReader(targetBlobWritingConsumer);
+        for (var placement : placements) {
+            reader.advance(placement.offset);
+            placement.write(target);
+        }
+        reader.advance(output.size());
+        this.targetBlob = null;
+        placeholderMarker = null;
+    }
+
+    private Blob targetBlob;
+    private BinaryDataConsumer targetBlobWritingConsumer = (data, offset, limit) ->
+            targetBlob.write(data, offset, limit);
+
+    private static abstract class Placement {
+        int offset;
+
+        Placement(int offset) {
+            this.offset = offset;
+        }
+
+        abstract void write(Blob blob);
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholder.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholder.java
new file mode 100644
index 000000000..020fded80
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholder.java
@@ -0,0 +1,47 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.teavm.backend.wasm.dwarf.blob.Marker;
+
+public class DwarfPlaceholder {
+    int ptr = -1;
+    final int size;
+    List<ForwardRef> forwardReferences;
+
+    DwarfPlaceholder(int size) {
+        this.size = size;
+    }
+
+    void addForwardRef(DwarfPlaceholderWriter writer, Marker marker) {
+        if (forwardReferences == null) {
+            forwardReferences = new ArrayList<>();
+            forwardReferences.add(new ForwardRef(writer, marker));
+        }
+    }
+
+    static class ForwardRef {
+        final DwarfPlaceholderWriter writer;
+        final Marker marker;
+
+        ForwardRef(DwarfPlaceholderWriter writer, Marker marker) {
+            this.writer = writer;
+            this.marker = marker;
+        }
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholderWriter.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholderWriter.java
new file mode 100644
index 000000000..7099f3ae2
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/DwarfPlaceholderWriter.java
@@ -0,0 +1,22 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf;
+
+import org.teavm.backend.wasm.dwarf.blob.Blob;
+
+public interface DwarfPlaceholderWriter {
+    void write(Blob blob, int actualValue);
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BinaryDataConsumer.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BinaryDataConsumer.java
new file mode 100644
index 000000000..7d61ad411
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BinaryDataConsumer.java
@@ -0,0 +1,20 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf.blob;
+
+public interface BinaryDataConsumer {
+    void accept(byte[] data, int offset, int limit);
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Blob.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Blob.java
new file mode 100644
index 000000000..f20921c9a
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Blob.java
@@ -0,0 +1,158 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf.blob;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Blob {
+    private byte[] buffer = new byte[16];
+    byte[] currentChunk = new byte[4096];
+    List<byte[]> data = new ArrayList<>();
+    int chunkIndex;
+    int posInChunk;
+    int ptr;
+    private int size;
+
+    public Blob() {
+        data.add(currentChunk);
+    }
+
+    public Blob write(byte[] bytes) {
+        return write(bytes, 0, bytes.length);
+    }
+
+    public Blob write(byte[] bytes, int offset, int limit) {
+        if (offset == limit) {
+            return this;
+        }
+        if (offset + 1 == limit) {
+            write(bytes[offset]);
+            return this;
+        }
+        while (offset < limit) {
+            var remaining = Math.min(limit - offset, currentChunk.length - posInChunk);
+            System.arraycopy(bytes, offset, currentChunk, posInChunk, remaining);
+            posInChunk += remaining;
+            offset += remaining;
+            ptr += remaining;
+            nextChunkIfNeeded();
+        }
+        size = Math.max(size, ptr);
+        return this;
+    }
+
+    public Blob skip(int count) {
+        while (count > 0) {
+            var remaining = Math.min(count, currentChunk.length - posInChunk);
+            posInChunk += remaining;
+            ptr += remaining;
+            count -= remaining;
+            nextChunkIfNeeded();
+        }
+        size = Math.max(size, ptr);
+        return this;
+    }
+
+    public Blob write(byte b) {
+        currentChunk[posInChunk++] = b;
+        ptr++;
+        nextChunkIfNeeded();
+        size = Math.max(size, ptr);
+        return this;
+    }
+
+    public Blob writeInt(int value) {
+        var buffer = this.buffer;
+        buffer[0] = (byte) value;
+        buffer[1] = (byte) (value >>> 8);
+        buffer[2] = (byte) (value >>> 16);
+        buffer[3] = (byte) (value >>> 24);
+        return write(buffer, 0, 4);
+    }
+
+    public Blob writeShort(int value) {
+        var buffer = this.buffer;
+        buffer[0] = (byte) value;
+        buffer[1] = (byte) (value >>> 8);
+        return write(buffer, 0, 2);
+    }
+
+    public Blob writeByte(int value) {
+        return write((byte) value);
+    }
+
+    public Blob writeLEB(int value) {
+        var ptr = 0;
+        var buffer = this.buffer;
+        while ((value & 0x7F) != value) {
+            buffer[ptr++] = (byte) ((value & 0x7F) | 0x80);
+            value >>= 7;
+        }
+        buffer[ptr++] = (byte) (value & 0x7F);
+        return write(buffer, 0, ptr);
+    }
+
+    private void nextChunkIfNeeded() {
+        if (posInChunk < currentChunk.length) {
+            return;
+        }
+        posInChunk = 0;
+        if (++chunkIndex >= data.size()) {
+            currentChunk = new byte[currentChunk.length];
+            data.add(currentChunk);
+        } else {
+            currentChunk = data.get(chunkIndex);
+        }
+    }
+
+    public int chunkCount() {
+        return data.size() + 1;
+    }
+
+    public BlobReader newReader(BinaryDataConsumer consumer) {
+        return new BlobReader(this, consumer);
+    }
+
+    public Marker marker() {
+        return new Marker(this, chunkIndex, posInChunk, ptr);
+    }
+
+    public byte[] chunkAt(int index) {
+        return index < data.size() ? data.get(index) : currentChunk;
+    }
+
+    public int ptr() {
+        return ptr;
+    }
+
+    public int size() {
+        return size;
+    }
+
+    public byte[] toArray() {
+        var result = new byte[size];
+        var ptr = 0;
+        for (var chunk : data) {
+            int bytesToCopy = Math.min(chunk.length, size - ptr);
+            if (bytesToCopy > 0) {
+                System.arraycopy(chunk, 0, result, ptr, bytesToCopy);
+                ptr += bytesToCopy;
+            }
+        }
+        return result;
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BlobReader.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BlobReader.java
new file mode 100644
index 000000000..7ce3d255c
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/BlobReader.java
@@ -0,0 +1,61 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf.blob;
+
+public class BlobReader {
+    private Blob output;
+    private BinaryDataConsumer consumer;
+    private int ptr;
+    private int currentChunk;
+    private int offsetInChunk;
+
+    BlobReader(Blob output, BinaryDataConsumer consumer) {
+        this.output = output;
+        this.consumer = consumer;
+    }
+
+    public int position() {
+        return ptr;
+    }
+
+    public void advance(int to) {
+        if (to < ptr || to > output.size()) {
+            throw new IllegalArgumentException();
+        }
+        if (to == ptr) {
+            return;
+        }
+
+        var ptr = this.ptr;
+        var currentChunk = this.currentChunk;
+        var offsetInChunk = this.offsetInChunk;
+        while (ptr < to) {
+            var chunk = output.chunkAt(currentChunk);
+            var limit = Math.min(ptr + chunk.length, to);
+            var bytesToWrite = limit - ptr;
+            consumer.accept(chunk, offsetInChunk, offsetInChunk + bytesToWrite);
+            offsetInChunk += bytesToWrite;
+            ptr += bytesToWrite;
+            if (offsetInChunk == chunk.length) {
+                offsetInChunk = 0;
+                currentChunk++;
+            }
+        }
+        this.ptr = ptr;
+        this.currentChunk = currentChunk;
+        this.offsetInChunk = offsetInChunk;
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Marker.java b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Marker.java
new file mode 100644
index 000000000..1e331eef7
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/dwarf/blob/Marker.java
@@ -0,0 +1,43 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.dwarf.blob;
+
+public class Marker {
+    private Blob blob;
+    private int chunkIndex;
+    private int posInChunk;
+    private int ptr;
+
+    Marker(Blob blob, int chunkIndex, int posInChunk, int ptr) {
+        this.blob = blob;
+        this.chunkIndex = chunkIndex;
+        this.posInChunk = posInChunk;
+        this.ptr = ptr;
+    }
+
+    public void rewind() {
+        blob.chunkIndex = chunkIndex;
+        blob.posInChunk = posInChunk;
+        blob.ptr = ptr;
+        blob.currentChunk = blob.data.get(chunkIndex);
+    }
+
+    public void update() {
+        chunkIndex = blob.chunkIndex;
+        posInChunk = blob.posInChunk;
+        ptr = blob.ptr;
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/generate/DwarfGenerator.java b/core/src/main/java/org/teavm/backend/wasm/generate/DwarfGenerator.java
new file mode 100644
index 000000000..ed6acf86a
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/generate/DwarfGenerator.java
@@ -0,0 +1,83 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.generate;
+
+import static org.teavm.backend.wasm.dwarf.DwarfConstants.DWARF_VERSION;
+import static org.teavm.backend.wasm.dwarf.DwarfConstants.DW_TAG_COMPILE_UNIT;
+import static org.teavm.backend.wasm.dwarf.DwarfConstants.DW_UT_COMPILE;
+import java.util.Arrays;
+import java.util.Collection;
+import org.teavm.backend.wasm.dwarf.DwarfInfoWriter;
+import org.teavm.backend.wasm.dwarf.DwarfPlaceholder;
+import org.teavm.backend.wasm.dwarf.blob.Blob;
+import org.teavm.backend.wasm.model.WasmCustomSection;
+
+public class DwarfGenerator {
+    private DwarfInfoWriter infoWriter = new DwarfInfoWriter();
+    private DwarfPlaceholder endOfSection;
+
+    public void begin() {
+        endOfSection = infoWriter.placeholder(4);
+        emitUnitHeader();
+        compilationUnit();
+    }
+
+    private void emitUnitHeader() {
+        // unit_length
+        infoWriter.ref(endOfSection, (blob, ptr) -> {
+            int size = ptr - blob.ptr() - 4;
+            blob.writeInt(size);
+        });
+
+        // version
+        infoWriter.writeShort(DWARF_VERSION);
+
+        // unit_type
+        infoWriter.writeByte(DW_UT_COMPILE);
+
+        // address_size
+        infoWriter.writeByte(4);
+
+        // debug_abbrev_offset
+        infoWriter.writeInt(0);
+    }
+
+    private void compilationUnit() {
+        infoWriter.tag(infoWriter.abbreviation(DW_TAG_COMPILE_UNIT, true, data -> { }));
+    }
+
+    public void end() {
+        closeTag(); // compilation unit
+        infoWriter.mark(endOfSection);
+    }
+
+    private void closeTag() {
+        infoWriter.writeByte(0);
+    }
+
+    public Collection<? extends WasmCustomSection> createSections() {
+        var abbreviations = new Blob();
+        infoWriter.buildAbbreviations(abbreviations);
+
+        var info = new Blob();
+        infoWriter.build(info);
+
+        return Arrays.asList(
+                new WasmCustomSection(".debug_abbrev", abbreviations.toArray()),
+                new WasmCustomSection(".debug_info", info.toArray())
+        );
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/model/WasmCustomSection.java b/core/src/main/java/org/teavm/backend/wasm/model/WasmCustomSection.java
new file mode 100644
index 000000000..a278b45f5
--- /dev/null
+++ b/core/src/main/java/org/teavm/backend/wasm/model/WasmCustomSection.java
@@ -0,0 +1,35 @@
+/*
+ *  Copyright 2022 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.backend.wasm.model;
+
+public class WasmCustomSection {
+    private String name;
+    private byte[] data;
+    WasmModule module;
+
+    public WasmCustomSection(String name, byte[] data) {
+        this.name = name;
+        this.data = data;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public byte[] getData() {
+        return data;
+    }
+}
diff --git a/core/src/main/java/org/teavm/backend/wasm/model/WasmModule.java b/core/src/main/java/org/teavm/backend/wasm/model/WasmModule.java
index 727672092..1a80b814d 100644
--- a/core/src/main/java/org/teavm/backend/wasm/model/WasmModule.java
+++ b/core/src/main/java/org/teavm/backend/wasm/model/WasmModule.java
@@ -29,6 +29,8 @@ public class WasmModule {
     private Map<String, WasmFunction> readonlyFunctions = Collections.unmodifiableMap(functions);
     private List<WasmFunction> functionTable = new ArrayList<>();
     private WasmFunction startFunction;
+    private Map<String, WasmCustomSection> customSections = new LinkedHashMap<>();
+    private Map<String, WasmCustomSection> readonlyCustomSections = Collections.unmodifiableMap(customSections);
 
     public void add(WasmFunction function) {
         if (functions.containsKey(function.getName())) {
@@ -53,6 +55,30 @@ public class WasmModule {
         return readonlyFunctions;
     }
 
+    public void add(WasmCustomSection customSection) {
+        if (customSections.containsKey(customSection.getName())) {
+            throw new IllegalArgumentException("Custom section " + customSection.getName()
+                    + " already defined in this module");
+        }
+        if (customSection.module != null) {
+            throw new IllegalArgumentException("Given custom section is already registered in another module");
+        }
+        customSections.put(customSection.getName(), customSection);
+        customSection.module = this;
+    }
+
+    public void remove(WasmCustomSection customSection) {
+        if (customSection.module != this) {
+            return;
+        }
+        customSection.module = null;
+        customSections.remove(customSection.getName());
+    }
+
+    public Map<? extends String, ? extends WasmCustomSection> getCustomSections() {
+        return readonlyCustomSections;
+    }
+
     public List<WasmFunction> getFunctionTable() {
         return functionTable;
     }
diff --git a/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java b/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java
index 42293ce9c..67581660d 100644
--- a/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java
+++ b/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java
@@ -17,10 +17,13 @@ package org.teavm.backend.wasm.render;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
+import org.teavm.backend.wasm.model.WasmCustomSection;
 import org.teavm.backend.wasm.model.WasmFunction;
 import org.teavm.backend.wasm.model.WasmLocal;
 import org.teavm.backend.wasm.model.WasmMemorySegment;
@@ -58,6 +61,10 @@ public class WasmBinaryRenderer {
     }
 
     public void render(WasmModule module) {
+        render(module, Collections::emptyList);
+    }
+
+    public void render(WasmModule module, Supplier<Collection<? extends WasmCustomSection>> customSectionSupplier) {
         output.writeInt32(0x6d736100);
         switch (version) {
             case V_0x1:
@@ -78,6 +85,7 @@ public class WasmBinaryRenderer {
         if (!obfuscated) {
             renderNames(module);
         }
+        renderCustomSections(module, customSectionSupplier);
     }
 
     private void renderSignatures(WasmModule module) {
@@ -354,6 +362,22 @@ public class WasmBinaryRenderer {
         writeSection(SECTION_UNKNOWN, "name", section.getData());
     }
 
+    private void renderCustomSections(WasmModule module,
+            Supplier<Collection<? extends WasmCustomSection>> sectionSupplier) {
+        for (var customSection : module.getCustomSections().values()) {
+            renderCustomSection(customSection);
+        }
+        if (sectionSupplier != null) {
+            for (var customSection : sectionSupplier.get()) {
+                renderCustomSection(customSection);
+            }
+        }
+    }
+
+    private void renderCustomSection(WasmCustomSection customSection) {
+        writeSection(SECTION_UNKNOWN, customSection.getName(), customSection.getData());
+    }
+
     static class LocalEntry {
         WasmType type;
         int count = 1;
diff --git a/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js b/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js
index f6b938ba2..699308ac1 100644
--- a/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js
+++ b/core/src/main/resources/org/teavm/backend/wasm/wasm-runtime.js
@@ -31,9 +31,9 @@ TeaVM.wasm = function() {
             lineBuffer += String.fromCharCode(charCode);
         }
     }
-    function putwchars(buffer, count) {
+    function putwchars(controller, buffer, count) {
         let instance = controller.instance;
-        let memory = instance.exports.memory.buffer;
+        let memory = new Int8Array(instance.exports.memory.buffer);
         for (let i = 0; i < count; ++i) {
             // TODO: support UTF-8
             putwchar(memory[buffer++]);