프로젝트/APM prototype 개발

[A Java bytecode engineering library] - [Tree API] 8. Method Analysis

블랑v 2024. 2. 22. 23:05

 

8. Method Analysis

이 장에서는 메소드의 코드를 분석하는 ASM API를 소개한다. 이 API는 트리 API를 기반으로 한다. 코드 분석 알고리즘 소개로 시작하여, 해당 ASM API와 일부 예시를 제시한다.

8.1. 소개

코드 분석은 매우 광범위한 주제이며, 코드를 분석하기 위한 많은 알고리즘이 존재한다. 여기서 모두를 소개하는 것은 불가능하며 이 문서의 범위를 벗어난다. 사실 이 섹션의 목적은 단지 ASM에서 사용되는 알고리즘에 대한 개요를 제공하는 것이다. 이 주제에 대한 더 나은 소개는 컴파일러에 관한 책에서 찾을 수 있다. 다음 섹션에서는 데이터 흐름과 제어 흐름 분석이라는 두 가지 중요한 코드 분석 기술을 소개한다:

 

  • 데이터 흐름 분석( data flow analysis )은 메소드의 각 명령어에 대해 실행 프레임의 상태를 계산하는 것으로 구성된다. 이 상태는 더 간단하거나 복잡한 방식으로 표현될 수 있다. 예를 들어, 참조 값은 하나의 값, 클래스별 하나의 값, { null, not null, may be null } 세트의 세 가지 가능한 값 등으로 표현될 수 있다.
  • 제어 흐름 분석( control flow analysis )은 메소드의 제어 흐름 그래프를 계산하고 이 그래프에 대한 분석을 수행하는 것으로 구성된다. 제어 흐름 그래프는 노드가 명령어이고, 두 명령어 i → j가 j가 i 바로 후에 실행될 수 있을 때 연결된 방향성 엣지를 가진 그래프이다.

8.1.1. 데이터 흐름 분석(Data flow analyses)

두 가지 유형의 데이터 흐름 분석을 수행할 수 있다:

  • 전방 분석은 각 명령어에 대해, 해당 명령어의 실행 전 상태에서 실행 후의 실행 프레임 상태를 계산한다.
  • 역방향 분석은 각 명령어에 대해, 해당 명령어의 실행 후 상태에서 실행 전의 실행 프레임 상태를 계산한다.

전방 데이터 흐름 분석은 메소드의 각 바이트코드 명령어를 실행 프레임에서 시뮬레이션함으로써 수행된다.

이는 일반적으로 다음을 포함한다:

  • 스택에서 값 추출,
  • 이를 결합,
  • 결과를 스택에 푸시.

이는 인터프리터나 자바 가상 머신이 하는 것과 비슷해 보이지만, 실제로는 완전히 다르다.

왜냐하면 목표는 특정 메소드 인자 값에 의해 결정되는 단일 실행 경로가 아니라, 모든 가능한 인자 값에 대해 메소드의 모든 잠재적 실행 경로를 시뮬레이션하는 것이기 때문이다. 한 가지 결과는 분기 명령어의 경우, 두 가지 분기 모두를 시뮬레이션한다는 것이다(실제 인터프리터는 실제 조건 값에 따라 한 가지 분기만 따른다).

 

또 다른 결과는 조작되는 값이 실제로 가능한 값들의 집합이라는 것이다.

이러한 집합은 "모든 가능한 값", "모든 정수", "모든 가능한 객체" 또는 "모든 가능한 String 객체"와 같이 매우 크게 설정될 수 있다. 이 경우 이들은 타입이라고도 할 수 있다. 또한 "모든 양의 정수", "0과 10 사이의 모든 정수" 또는 "모든 가능한 non null 객체"와 같이 더 정확할 수도 있다.

명령어 i의 실행을 시뮬레이션하는 것은 i의 피연산자 값 세트의 모든 조합에 대한 i의 모든 가능한 결과 집합을 찾는 것을 의미한다. 예를 들어, 정수가 P = "양수 또는 null", N = "음수 또는 null", A = "모든 정수"의 세 가지 세트로 표현된다면, IADD 명령어의 시뮬레이션은 두 피연산자가 모두 P인 경우 P를 반환하고, 두 피연산자가 모두 N인 경우 N을 반환하며, 그 외의 모든 경우에는 A를 반환한다는 것을 의미한다.

 

마지막 결과는 값 세트의 합집합을 계산할 필요가 있다는 것이다:

예를 들어, (b ? e1 : e2)에 해당하는 가능한 값 세트는 e1의 가능한 값과 e2의 가능한 값의 합집합이다. 제어 흐름 그래프에 공통 목적지를 가진 두 개 이상의 엣지가 포함될 때마다 이 작업이 필요하다. 이전 예제에서 정수가 세 가지 세트 P, N, A로 표현된다면, 이 두 세트의 합집합을 계산하는 것은 쉽다: 두 세트가 같지 않은 한 항상 A이다.

 

8.1.2. 제어 흐름 분석( control flow analysis)

제어 흐름 분석은 메소드의 제어 흐름 그래프에 기반한 분석이다. 예를 들어, 섹션 3.1.3의 checkAndSetF 메소드의 제어 흐름 그래프는 다음과 같이 제공된다(그래프에 실제 명령어처럼 포함된 레이블 포함):

 

 

이 그래프는 네 개의 기본 블록(위에 사각형으로 표시됨)으로 분해될 수 있는데, 기본 블록은 마지막 명령어를 제외한 각 명령어가 정확히 하나의 후속자를 가지며, 첫 번째 명령어를 제외한 어떤 명령어도 점프의 대상이 될 수 없는 명령어 시퀀스이다.

 

8.2. 인터페이스 및 컴포넌트

코드 분석을 위한 ASM API는 org.objectweb.asm.tree.analysis 패키지에 있다. 패키지 이름이 시사하듯이, 이는 트리 API를 기반으로 한다. 사실 이 패키지는 전방 데이터 흐름 분석을 수행하기 위한 프레임워크를 제공한다.

더 정밀한 값 세트로 다양한 데이터 흐름 분석을 수행할 수 있도록 하기 위해, 데이터 흐름 분석 알고리즘은 고정된 부분과 사용자에 의해 제공되는 가변적인 부분으로 나뉜다. 보다 구체적으로:

 

  • 전반적인 데이터 흐름 분석 알고리즘과 스택에서 적절한 수의 값을 팝하고 푸시하는 작업은 Analyzer 및 Frame 클래스에서 한 번에 구현된다.
  • 값 결합 및 값 세트 합집합 계산 작업은 Interpreter 및 Value 추상 클래스의 사용자 정의 하위 클래스에서 수행된다. 몇 가지 미리 정의된 하위 클래스가 제공되며, 다음 섹션에서 설명된다.

프레임워크의 기본 목표는 데이터 흐름 분석을 수행하는 것이지만, Analyzer 클래스는 분석된 메소드의 제어 흐름 그래프도 구성할 수 있다. 이는 이 클래스의 newControlFlowEdge 및 newControlFlowExceptionEdge 메소드를 오버라이딩함으로써 수행될 수 있으며, 기본적으로 아무 것도 수행하지 않는다. 이 결과는 제어 흐름 분석을 수행하는 데 사용될 수 있다.

 

8.2.1. 기본 데이터 흐름 분석

BasicInterpreter 클래스는 Interpreter 추상 클래스의 미리 정의된 하위 클래스 중 하나이다. 이는 바이트코드 명령어의 효과를 BasicValue 클래스에서 정의된 일곱 가지 값 세트를 사용하여 시뮬레이션을 진행한다.

  • UNINITIALIZED_VALUE는 "모든 가능한 값"을 의미한다.
  • INT_VALUE는 "모든 int, short, byte, boolean 또는 char 값"을 의미한다.
  • FLOAT_VALUE는 "모든 float 값"을 의미한다.
  • LONG_VALUE는 "모든 long 값"을 의미한다.
  • DOUBLE_VALUE는 "모든 double 값"을 의미한다.
  • REFERENCE_VALUE는 "모든 객체 및 배열 값"을 의미한다.
  • RETURNADDRESS_VALUE는 서브루틴에 사용된다(부록 A.2 참조).

이 인터프리터는 자체적으로는 그다지 유용하지 않지만(메소드 프레임이 이미 더 자세한 정보를 제공한다 - 섹션 3.1.5 참조), Analyzer를 구성하기 위한 "빈(empty)" Interpreter 구현으로 사용될 수 있다.

이 분석기는 메소드에서 도달할 수 없는 코드를 감지하는 데 사용될 수 있다. 실제로, 점프 명령어의 두 분기를 모두 따르더라도, 첫 번째 명령어에서 도달할 수 없는 코드에 도달할 수 없다. 그 결과, 분석 후에는, Interpreter 구현에 관계없이, 도달할 수 없는 명령어에 대한 계산된 프레임 - Analyzer.getFrames 메소드에 의해 반환됨 - 은 null이다. 이 속성은 RemoveDeadCodeAdapter 클래스를 매우 쉽게 구현하는 데 사용될 수 있다(더 효율적인 방법이 있지만, 더 많은 코드를 작성해야 한다):

public class RemoveDeadCodeAdapter extends MethodVisitor {
    String owner;
    MethodVisitor next;
    public RemoveDeadCodeAdapter(String owner, int access, String name,
                                 String desc, MethodVisitor mv) {
        super(ASM4, new MethodNode(access, name, desc, null, null));
        this.owner = owner;
        next = mv;
    }
    @Override public void visitEnd() {
        MethodNode mn = (MethodNode) mv;
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new BasicInterpreter());
        try {
            a.analyze(owner, mn);
            Frame<BasicValue>[] frames = a.getFrames();
            AbstractInsnNode[] insns = mn.instructions.toArray();
            for (int i = 0; i < frames.length; ++i) {
                if (frames[i] == null && !(insns[i] instanceof LabelNode)) {
                    mn.instructions.remove(insns[i]);
                }
            }
        } catch (AnalyzerException ignored) {
        }
        mn.accept(next);
    }
}

 

jump optimizer 점프 최적화기에 의해 도입된 죽은 코드는 OptimizeJumpAdapter(섹션 7.1.5)와 결합될 때 제거된다. 예를 들어, 이 어댑터 체인을 checkAndSetF 메소드에 사용하면 다음과 같은 결과를 얻을 수 있다:

 

 

 

dead label이 제거되지 않는 것을 확인하자. 이것은 의도적으로 행해지는 것이다 :

실제로 결과의 코드는 변경되지 않지만( 도달할 수도 없지만) LocalVariableNode 등에서 참조될 수 있는 레이블을 제거하는 것을 피한다.

 

( Note that dead labels are not removed. This is done on purpose: indeed it does not change the resulting code, but avoids removing a label that, although not reachable, might be referenced in a LocalVariableNode, for instance)

 

8.2.2. 기본 데이터 흐름 검증기( Basic data flow verifier)

BasicVerifier 클래스는 BasicInterpreter 클래스를 확장한다. 이는 동일한 일곱 가지 세트를 사용하지만, BasicInterpreter와 달리 명령어가 올바르게 사용되는지 확인한다. 예를 들어, IADD 명령어의 피연산자가 INTEGER_VALUE 값인지 확인한다(반면 BasicInterpreter는 결과, 즉 INTEGER_VALUE만 반환한다). 이 클래스는 클래스 생성기 또는 어댑터의 개발 중 디버깅 목적으로 사용될 수 있다.

public class BasicVerifierAdapter extends MethodVisitor {
    String owner;
    MethodVisitor next;
    public BasicVerifierAdapter(String owner, int access, String name,
                                String desc, MethodVisitor mv) {
        super(ASM4, new MethodNode(access, name, desc, null, null));
        this.owner = owner;
        next = mv;
    }
    @Override public void visitEnd() {
        MethodNode mn = (MethodNode) mv;
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue(new BasicVerifier());
        try {
            a.analyze(owner, mn);
        } catch (AnalyzerException e) {
            throw new RuntimeException(e.getMessage());
        }
        mn.accept(next);
    }
}

8.2.3. 단순 데이터 흐름 검증기( Simple data flow verifier)

SimpleVerifier 클래스는 BasicVerifier 클래스를 확장한다.

이는 바이트코드 명령어의 실행을 시뮬레이션하기 위해 더 많은 세트를 사용한다: 실제로 각 클래스는 자체 세트로 표현되며, 이 클래스의 모든 가능한 객체를 나타낸다. 따라서 이는 String 클래스에서 정의된 메소드를 "모든 Thread 타입의 객체"의 가능한 값에 대한 객체에서 호출하는 것과 같은 더 많은 오류를 감지할 수 있다.

 

이 클래스는 클래스 계층과 관련된 검증 및 계산을 수행하기 위해 Java reflection API를 사용한다.

따라서 메소드에서 참조된 클래스를 JVM으로 로드한다. 이 기본 동작은 이 클래스의 보호된 메소드를 Override함으로써 변경될 수 있다.

 

BasicVerifier와 마찬가지로, 이 클래스는 클래스 생성기 또는 어댑터의 개발 중 버그를 더 쉽게 찾는 데 사용될 수 있다.

그러나 다른 목적으로도 사용될 수 있다. 예를 들어 메소드에서 불필요한 캐스트를 제거하는 변환을 보자:

 

이 분석기가 CHECKCAST 명령어의 피연산자가 "from 타입의 모든 객체"의 값 세트이고 to가 from의 상위 클래스인 경우, CHECKCAST 명령어는 불필요하며 제거될 수 있다는 것을 발견했다. 이 변환의 구현은 다음과 같다:

public class RemoveUnusedCastTransformer extends MethodTransformer {
    String owner;
    public RemoveUnusedCastTransformer(String owner,
                                       MethodTransformer mt) {
        super(mt);
        this.owner = owner;
    }
    @Override public MethodNode transform(MethodNode mn) {
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new SimpleVerifier());
        try {
            a.analyze(owner, mn);
            Frame<BasicValue>[] frames = a.getFrames();
            AbstractInsnNode[] insns = mn.instructions.toArray();
            for (int i = 0; i < insns.length; ++i) {
                AbstractInsnNode insn = insns[i];
                if (insn.getOpcode() == CHECKCAST) {
                    Frame f = frames[i];
                    if (f != null && f.getStackSize() > 0) {
                        Object operand = f.getStack(f.getStackSize() - 1);
                        Class<?> to = getClass(((TypeInsnNode) insn).desc);
                        Class<?> from = getClass(((BasicValue) operand).getType());
                        if (to.isAssignableFrom(from)) {
                            mn.instructions.remove(insn);
                        }
                    }
                }
            }
        } catch (AnalyzerException ignored) {
        }
        return mt == null ? mn : mt.transform(mn);
    }
    private static Class<?> getClass(String desc) {
        try {
            return Class.forName(desc.replace(’/’, ’.’));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e.toString());}
    }
    private static Class<?> getClass(Type t) {
        if (t.getSort() == Type.OBJECT) {
            return getClass(t.getInternalName());
        }
        return getClass(t.getDescriptor());
    }
}

 

그러나 자바 6 클래스(또는 COMPUT_FRAMES를 사용하여 자바 6으로 업그레이드된 클래스) 이상의 경우 핵심 API로 이를 수행하기 위해 AnalyzerAdapter를 사용하는 것이 더 간단하고 훨씬 효율적이다:

 

public class RemoveUnusedCastAdapter extends MethodVisitor {
    public AnalyzerAdapter aa;
    public RemoveUnusedCastAdapter(MethodVisitor mv) {
        super(ASM4, mv);
    }
    @Override public void visitTypeInsn(int opcode, String desc) {
        if (opcode == CHECKCAST) {
            Class<?> to = getClass(desc);
            if (aa.stack != null && aa.stack.size() > 0) {
                Object operand = aa.stack.get(aa.stack.size() - 1);
                if (operand instanceof String) {
                    Class<?> from = getClass((String) operand);
                    if (to.isAssignableFrom(from)) {
                        return;
                    }
                }
            }
        }
        mv.visitTypeInsn(opcode, desc);
    }
    private static Class getClass(String desc) {
        try {
            return Class.forName(desc.replace(’/’, ’.’));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e.toString());
        }
    }
}

 

8.2.4. 사용자 정의 데이터 흐름 분석( User defined data flow analysis)

가정해 보자. 잠재적으로 null인 객체에 대한 필드 접근이나 메소드 호출을 감지하고 싶다. 예를 들어, 다음 소스 코드 조각에서 첫 번째 줄은 일부 컴파일러가 버그를 감지하지 못하게 하여, 그렇지 않으면 "o가 초기화되지 않았을 수 있음" 오류로 감지될 것이다:

 

이를 위해서는 INVOKEVIRTUAL 명령어에 해당하는 마지막 줄에서, 스택의 바닥 값인 o가 null일 수 있다는 것을 알려주는 데이터 흐름 분석이 필요하다.

이를 위해 참조 값에 대해 세 가지 세트를 구분할 필요가 있다:

null 값을 포함하는 NULL 세트, 모든 non null 참조 값을 포함하는 NONNULL 세트, 모든 참조 값을 포함하는 MAYBENULL 세트.

 

그런 다음 ACONST_NULL이 피연산자 스택에 NULL 세트를 푸시하고, 스택에 참조 값을 푸시하는 모든 다른 명령어가 NONNULL 세트를 푸시한다고 가정하면 된다(즉, 어떤 필드 접근이나 메소드 호출의 결과도 null이 아니라고 가정한다). MAYBENULL 세트는 NULL과 NONNULL 세트의 합집합을 나타내기 위해 필요하다.

 

위 규칙은 사용자 정의 Interpreter 하위 클래스에서 구현되어야 한다. 이를 처음부터 구현할 수도 있지만, BasicInterpreter 클래스를 확장하여 구현하는 것이 가능하고 훨씬 쉽다. 실제로, BasicValue.REFERENCE_VALUE가 NONNULL 세트에 해당한다고 가정하면, ACONST_NULL의 실행을 시뮬레이션하는 메소드를 오버라이드하여 NULL을 반환하고, 세트 합집합을 계산하는 메소드를 오버라이드하기만 하면 된다.

 

class IsNullInterpreter extends BasicInterpreter {
    public final static BasicValue NULL = new BasicValue(null);
    public final static BasicValue MAYBENULL = new BasicValue(null);

    public IsNullInterpreter() {
        super(ASM4);
    }

    @Override
    public BasicValue newOperation(AbstractInsnNode insn) {
        if (insn.getOpcode() == ACONST_NULL) {
            return NULL;
        }
        return super.newOperation(insn);
    }

    @Override
    public BasicValue merge(BasicValue v, BasicValue w) {
        if (isRef(v) && isRef(w) && v != w) {
            return MAYBENULL;
        }
        return super.merge(v, w);
    }

    private boolean isRef(Value v) {
        return v == REFERENCE_VALUE || v == NULL || v == MAYBENULL;
    }
}

 

이 IsNullInterpreter를 사용하여 잠재적인 null 포인터 예외를 일으킬 수 있는 명령어를 감지하는 것은 쉽다:

 

public class NullDereferenceAnalyzer {
    public List<AbstractInsnNode> findNullDereferences(String owner,
                                                       MethodNode mn) throws AnalyzerException {
        List<AbstractInsnNode> result = new ArrayList<AbstractInsnNode>();
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new IsNullInterpreter());
        a.analyze(owner, mn);
        Frame<BasicValue>[] frames = a.getFrames();
        AbstractInsnNode[] insns = mn.instructions.toArray();
        for (int i = 0; i < insns.length; ++i) {
            AbstractInsnNode insn = insns[i];
            if (frames[i] != null) {
                Value v = getTarget(insn, frames[i]);
                if (v == NULL || v == MAYBENULL) {
                    result.add(insn);
                }
            }
        }
        return result;
    }
    private static BasicValue getTarget(AbstractInsnNode insn,
                                        Frame<BasicValue> f) {
        switch (insn.getOpcode()) {
            case GETFIELD:
            case ARRAYLENGTH:
            case MONITORENTER:
            case MONITOREXIT:
                return getStackValue(f, 0);
            case PUTFIELD:
                return getStackValue(f, 1);
            case INVOKEVIRTUAL:
            case INVOKESPECIAL:
            case INVOKEINTERFACE:
                String desc = ((MethodInsnNode) insn).desc;
                return getStackValue(f, Type.getArgumentTypes(desc).length);
        }
        return null;
    }
    private static BasicValue getStackValue(Frame<BasicValue> f,
                                        int index) {
    int top = f.getStackSize() - 1;
    return index <= top ? f.getStack(top - index) : null;
}
}

 

findNullDereferences 메소드는 IsNullInterpreter를 사용하여 주어진 메소드 노드를 분석한다. 그런 다음 각 명령어에 대해, 참조 피연산자(있는 경우)의 가능한 값 세트가 NULL 세트인지 NONNULL 세트인지를 테스트한다. 만약 그렇다면 이 명령어는 null 포인터 예외를 일으킬 수 있으므로, 이 메소드에 의해 반환되는 해당 명령어의 리스트에 추가된다.

 

getTarget 메소드는 insn의 객체 피연산자에 해당하는 Value를 프레임 f에서 반환하거나, insn에 객체 피연산자가 없는 경우 null을 반환한다. 주요 역할은 이 값이 피연산자 스택 상단에서부터 얼마나 떨어져 있는지를 계산하는 것이며, 이는 명령어의 유형에 따라 달라진다.

 

8.2.5. 제어 흐름 분석

제어 흐름 분석은 많은 응용 프로그램을 가질 수 있다.

간단한 예로, 메소드의 순환 복잡도를 계산하는 것이 있다. 이 메트릭은 제어 흐름 그래프의 엣지 수에서 노드 수를 뺀 다음 2를 더한 값으로 정의된다. 예를 들어, 제어 흐름 그래프가 섹션 8.1.2에 나온 checkAndSetF 메소드의 순환 복잡도는 11 - 12 + 2 = 1이다. 이 메트릭은 메소드의 "복잡성"에 대한 좋은 지표를 제공한다(이 숫자와 메소드의 평균 버그 수 사이에는 상관 관계가 있다). 또한 메소드를 "올바르게" 테스트하는 데 필요한 테스트 케이스의 권장 수를 제공한다.

 

이 메트릭을 계산하는 알고리즘은 ASM 분석 프레임워크로 구현할 수 있다(코어 API만을 기반으로 한 더 효율적인 방법이 있지만, 더 많은 코드를 작성해야 한다). 첫 번째 단계는 제어 흐름 그래프를 구성하는 것이다. 이 장의 시작에서 언급했듯이, 이는 Analyzer 클래스의 newControlFlowEdge 메소드를 오버라이딩함으로써 수행될 수 있다. 이 클래스는 Frame 객체로 노드를 나타낸다. 이 객체에 그래프를 저장하려면 Frame 클래스를 확장할 필요가 있다.

 

class Node<V extends Value> extends Frame<V> {
    Set<Node<V>> successors = new HashSet<Node<V>>();

    public Node(int nLocals, int nStack) {
        super(nLocals, nStack);
    }

    public Node(Frame<? extends V> src) {
        super(src);
    }
}

 

그런 다음 우리의 제어 흐름 그래프를 구성하는 Analyzer 하위 클래스를 제공하고, 그 결과를 사용하여 엣지의 수, 노드의 수를 계산하고, 최종적으로 순환 복잡도를 계산할 수 있다.

 

public class CyclomaticComplexity {
    public int getCyclomaticComplexity(String owner, MethodNode mn)
            throws AnalyzerException {
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new BasicInterpreter()) {
                    protected Frame<BasicValue> newFrame(int nLocals, int nStack) {
                        return new Node<BasicValue>(nLocals, nStack);
                    }
                    protected Frame<BasicValue> newFrame(
                            Frame<? extends BasicValue> src) {
                        return new Node<BasicValue>(src);
                    }
                    protected void newControlFlowEdge(int src, int dst) {
                        Node<BasicValue> s = (Node<BasicValue>) getFrames()[src];
                        s.successors.add((Node<BasicValue>) getFrames()[dst]);
                    }
                };
        a.analyze(owner, mn);
        Frame<BasicValue>[] frames = a.getFrames();
        int edges = 0;
        int nodes = 0;
        for (int i = 0; i < frames.length; ++i) {
            if (frames[i] != null) {
                edges += ((Node<BasicValue>) frames[i]).successors.size();
                nodes += 1;
            }
        }
        return edges - nodes + 2;
    }
}

Reference

 

https://asm.ow2.io/asm4-guide.pdf

 

ASM USER GUIDE

 

Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.

 

THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.